#!/usr/bin/php
<?php
/*
Copyright 2009 Guillaume Boudreau

This file is part of Greyhole.

Greyhole is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Greyhole is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Greyhole.  If not, see <http://www.gnu.org/licenses/>.
*/

include('includes/common.php');
include('includes/sql.php');

list($action, $options) = process_command_line();

if ($action == 'unknown') {
	print_usage();
}

process_config();

db_connect() or gh_log(CRITICAL, "Can't connect to $db_options->engine database.");
repair_tables();
db_migrate();

if ($action != 'stats' && $action != 'view-queue' && $action != 'debug' && $action != 'iostat' && $action != 'status' && $action != 'getuid') {
	if (exec("whoami") != 'root') {
		echo "You need to execute this as root.\n";
		exit(1);
	}
}

if ($action == 'getuid') {
	$uniq_id = set_uniq_id();
	echo $uniq_id;
	exit(0);
}
if ($action == 'iostat') {
	$devices_directories = array();
	foreach ($storage_pool_directories as $target_drive) {
		$device = exec("df " . escapeshellarg($target_drive) . " | awk '{print \$1}'");
		$device = preg_replace('@/dev/(sd[a-z])[0-9]+@', '\1', $device);
		$devices_directories[$device] = $target_drive;
	}

	while (TRUE) {
		unset($result);
		exec("iostat -p ALL -k 10 2 | grep '^sd[a-z] ' | awk '{print \$1,\$3,\$4}'", $result);
		$iostat = array();
		foreach ($result as $line) {
			$info = explode(' ', $line);
			$device = $info[0];
			$read_kBps = $info[1];
			$write_kBps = $info[2];
			if (!isset($devices_directories[$device])) {
				# That device isn't in the storage pool.
				continue;
			}
			$directory = $devices_directories[$device];
			$iostat[$directory] = (float) $read_kBps + $write_kBps;
		}
		foreach ($iostat as $dir => $io_kBps) {
			echo "$dir: $io_kBps kBps\n";
		}
	}
	
	exit(0);
}

if ($action == 'thaw') {
	if (!isset($options['dir']) || array_search($options['dir'], $frozen_directories) === FALSE) {
		echo "Frozen directories:\n";
		foreach ($frozen_directories as $frozen_directory) {
			echo "  $frozen_directory\n";
		}
		echo "To thaw any of the above directories, use the following command:\n";
		echo "greyhole --thaw=directory\n";
		exit(0);
	}
	$path = explode('/', $options['dir']);
	$share = array_shift($path);
	$query = sprintf("UPDATE tasks SET complete = 'thawed' WHERE complete = 'frozen' AND share = '%s' AND full_path LIKE '%s%%'",
		db_escape_string($share),
		db_escape_string(implode('/', $path))
	);
	db_query($query) or die("Can't thaw tasks with query: $query - Error: " . db_error());
	echo $options['dir'] . " directory has been thawed.\n";
	echo "All pasts file operations that occured in this directory will now be processed by Greyhole.\n";
	exit(0);
}

if ($action == 'gone' || $action == 'going') {
	global $storage_pool_directories;
	if (array_search($options['dir'], $storage_pool_directories) === FALSE) {
		if (!empty($options['dir'])) {
			echo "Directory " . $options['dir'] . " is not one of your defined storage pool directories.\n";
		}
		echo "Please use one of the following with the --$action option:\n  ";
		echo implode("\n  ", $storage_pool_directories) . "\n";
		echo "Note that the correct syntax for this command is:\n";
		echo "  greyhole --$action=<directory>\n";
		echo "The '=' character is mandatory.\n";
		exit(1);
	}

	if ($action == 'going') {
		set_graveyard_backup();
		gh_log(INFO, "Storage pool directory " . $options['dir'] . " will be removed from the storage pool.");
		echo("Storage pool directory " . $options['dir'] . " will be removed from the storage pool.\n");

		// global $going_dir; // Used in function is_greyhole_owned_dir()
		$going_dir = $options['dir'];

		// Renaming that file will insure this drive won't be used while we're moving files away
		rename("$going_dir/.greyhole_uses_this", "$going_dir/.greyhole_used_this");

		// fsck shares with only 1 file copy to remove those from $options['dir']
		initialize_fsck_report('Shares with only 1 copy');
		foreach ($shares_options as $share_name => $share_options) {
			if ($share_options['num_copies'] == 1) {
				echo "Moving file copies for share '$share_name'... Please be patient... ";
				gh_fsck($share_options['landing_zone'], $share_name);
				echo "Done.\n";
			}
		}
	}

	// Remove $options['dir'] from config file and restart (if it was running)
	$escaped_dir = str_replace('/', '\/', $going_dir);
	exec("/bin/sed -ie 's/^.*storage_pool_directory.*$escaped_dir.*$//' /etc/greyhole.conf");
	exec("/sbin/service greyhole condrestart");

	// For Amahi users
	if (file_exists('/usr/bin/hdactl')) {
		echo "You should de-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page.\n";
	}

	mark_gone_ok($options['dir'], 'remove');
	mark_gone_drive_fscked($options['dir'], 'remove');
	gh_log(INFO, "Storage pool directory " . $options['dir'] . " has been marked Gone");
	echo "Storage pool directory " . $options['dir'] . " has been marked Gone, which means the missing file copies that are in this directory will be re-created during the next fsck.\n";

	if ($action == 'going') {
		// Schedule fsck for all shares to re-create missing copies on other shares
		db_query("INSERT INTO tasks (action, share, additional_info, complete) VALUES ('fsck', '', NULL, 'yes')") or gh_log(CRITICAL, "Can't insert fsck task: " . db_error());
		echo "All the files that were only on $going_dir have been copied somewhere else.\n";
		echo "A fsck of all shares has been scheduled, to recreate other file copies. It will start after all currently pending tasks have been completed.\n";
	} else { // $action == 'gone'
		echo "Sadly, file copies that were only on this drive, if any, are now lost!\n";
	}
	exit(0);
}

if ($action == 'debug') {
	if (!isset($options['debug_filename'])) {
		print_usage();
	}
	$filename = $options['debug_filename'];
	
	if (mb_strpos($filename, '/') === FALSE) {
		$filename = "/$filename";
	}

	echo "Debugging file operations for file named \"$filename\"\n";

	echo "\nFrom DB\n=======\n";
	$debug_tasks = array();
	$query = sprintf("SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE full_path LIKE '%%%s%%' ORDER BY id ASC",
		db_escape_string($filename)
	);
	$result = db_query($query) or die("Can't query tasks_completed with query: $query - Error: " . db_error());
	while ($row = db_fetch_object($result)) {
		$debug_tasks[$row->id] = $row;
	}

	// Renames
	$query = sprintf("SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE additional_info LIKE '%%%s%%' ORDER BY id ASC",
		db_escape_string($filename)
	);
	while (TRUE) {
		$result = db_query($query) or die("Can't query tasks_completed for renames with query: $query - Error: " . db_error());
		while ($row = db_fetch_object($result)) {
			$debug_tasks[$row->id] = $row;
			$query = sprintf("SELECT id, action, share, full_path, additional_info, event_date FROM tasks_completed WHERE additional_info = '%s' ORDER BY id ASC",
				db_escape_string($row->full_path)
			);
		}

		# Is there more?
		$new_query = preg_replace('/SELECT .* FROM/i', 'SELECT COUNT(*) FROM', $query);
		$result = db_query($new_query) or die("Can't query tasks_completed for COUNT of renames with query: $new_query - Error: " . db_error());
		if (db_fetch_object($result) !== FALSE) {
			break;
		}
	}
	
	ksort($debug_tasks);
	$to_grep = array();
	foreach ($debug_tasks as $task) {
		echo "  [$task->event_date] Task ID $task->id: $task->action $task->share/$task->full_path" . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : '') . "\n";
		$to_grep["$task->share/$task->full_path"] = 1;
		if ($task->action == 'rename') {
			$to_grep["$task->share/$task->additional_info"] = 1;
		}
	}
	if (empty($to_grep)) {
		$to_grep[$filename] = 1;
		if (mb_strpos($filename, '/') !== FALSE) {
			$share = trim(mb_substr($filename, 0, mb_strpos(mb_substr($filename, 1), '/')+1), '/');
			$full_path = trim(mb_substr($filename, mb_strpos(mb_substr($filename, 1), '/')+1), '/');
			$debug_tasks[] = (object) array('share' => $share, 'full_path' => $full_path);
		}
	}

	echo "\nFrom logs\n=========\n";
	$to_grep = array_keys($to_grep);
	$to_grep = implode("|", $to_grep);
	$commands = array();
	$commands[] = "zgrep -E -B 1 -A 1 -h " . escapeshellarg($to_grep) . " $greyhole_log_file*.gz";
	$commands[] = "grep -E -B 1 -A 1 -h " . escapeshellarg($to_grep) . " " . escapeshellarg($greyhole_log_file);
	foreach ($commands as $command) {
		exec($command, $result);
	}
	foreach ($result as $rline) {
		echo preg_replace('@^/var/log/greyhole.log[0-9\.gz-]*:@', '', $rline) . "\n";
	}
	
	echo "\nFrom filesystem\n===============\n";

	$last_task = array_pop($debug_tasks);
	$share = $last_task->share;
	if ($last_task->action == 'rename') {
		$full_path = $last_task->additional_info;
	} else {
		$full_path = $last_task->full_path;
	}
	list($path, $filename) = explode_full_path($full_path);
	echo "Landing Zone:\n";
	echo "  "; passthru("ls -l " . escapeshellarg(get_share_landing_zone($share) . "/" . $full_path));

	echo "\nGraveyard:\n";
	foreach ($storage_pool_directories as $target_drive) {
		$graveyard = clean_dir("$target_drive/.gh_graveyard");
		if (file_exists("$graveyard/$share/$full_path")) {
			echo "  "; passthru("ls -l " . escapeshellarg("$graveyard/$share/$full_path"));
			$data = var_export(unserialize(file_get_contents("$graveyard/$share/$full_path")), TRUE);
			$data = str_replace("\n", "\n    ", $data);
			echo "    $data\n";
		}
	}

	echo "\nFile copies:\n";
	foreach ($storage_pool_directories as $target_drive) {
		if (file_exists("$target_drive/$share/$full_path")) {
			echo "  "; passthru("ls -l " . escapeshellarg("$target_drive/$share/$full_path"));
		}
	}

	exit(0);
}

if ($action == 'empty-attic') {
	foreach ($storage_pool_directories as $target_drive) {
		$attic_path = clean_dir("$target_drive/.gh_attic");
		if (!file_exists($attic_path)) {
			echo "Attic in $target_drive is empty. Nothing to do.\n";
		} else {
			$attic_size = trim(exec("du -sk " . escapeshellarg($attic_path) . " | awk '{print $1}'"));
			echo "Attic in $target_drive is " . bytes_to_human($attic_size*1024, FALSE) . ". Emptying... ";
			exec("rm -rf " . escapeshellarg($attic_path));
			echo "Done\n";
		}
	}
	if (isset($attic_share) && mb_strlen(escapeshellarg($attic_share['landing_zone'])) > 8) {
		exec("rm -rf " . escapeshellarg($attic_share['landing_zone']) . '/*');
	}
	exit(0);
}

if ($action == 'view-queue') {
	$shares_names = array_keys($shares_options);
	natcasesort($shares_names);

	$max_share_strlen = 7;
	foreach ($shares_names as $share_name) {
		if (mb_strlen($share_name) > $max_share_strlen) {
			$max_share_strlen = mb_strlen($share_name);
		}
	}

	$queues = array();
	$total_num_writes_pending = $total_num_delete_pending = $total_num_rename_pending = $total_num_fsck_pending = 0;
	foreach ($shares_names as $share_name) {
		$result = db_query(sprintf("SELECT COUNT(*) AS num FROM tasks WHERE action = 'write' AND share = '%s' AND complete IN ('yes', 'thawed')", db_escape_string($share_name))) or die("Can't find # of writes in tasks table: " . db_error());
		$row = db_fetch_object($result);
		$num_writes_pending = (int) $row->num;
		$total_num_writes_pending += $num_writes_pending;

		$result = db_query(sprintf("SELECT COUNT(*) AS num FROM tasks WHERE (action = 'unlink' OR action = 'rmdir') AND share = '%s' AND complete IN ('yes', 'thawed')", db_escape_string($share_name))) or die("Can't find # of deletes in tasks table: " . db_error());
		$row = db_fetch_object($result);
		$num_delete_pending = (int) $row->num;
		$total_num_delete_pending += $num_delete_pending;

		$result = db_query(sprintf("SELECT COUNT(*) AS num FROM tasks WHERE action = 'rename' AND share = '%s' AND complete IN ('yes', 'thawed')", db_escape_string($share_name))) or die("Can't find # of renames in tasks table: " . db_error());
		$row = db_fetch_object($result);
		$num_rename_pending = (int) $row->num;
		$total_num_rename_pending += $num_rename_pending;

		$result = db_query(sprintf("SELECT COUNT(*) AS num FROM tasks WHERE (action = 'fsck' OR action = 'fsck_file') AND share = '%s' AND complete IN ('yes', 'thawed')", db_escape_string($share_name))) or die("Can't find # of fsck in tasks table: " . db_error());
		$row = db_fetch_object($result);
		$num_fsck_pending = (int) $row->num;
		$total_num_fsck_pending += $num_fsck_pending;

		$queues[$share_name] = (object) array(
			'num_writes_pending' => $num_writes_pending,
			'num_delete_pending' => $num_delete_pending,
			'num_rename_pending' => $num_rename_pending,
			'num_fsck_pending' => $num_fsck_pending,
		);
	}
	$queues['Total'] = (object) array(
		'num_writes_pending' => $total_num_writes_pending,
		'num_delete_pending' => $total_num_delete_pending,
		'num_rename_pending' => $total_num_rename_pending,
		'num_fsck_pending' => $total_num_fsck_pending,
	);
	
	$queues['Spooled'] = (int) exec("ls -1 /var/spool/greyhole | wc -l");

	if (isset($options['json'])) {
		echo json_encode($queues);
	} else {
		echo "\nGreyhole Work Queue Statistics\n==============================\n\n";
		echo "This table gives you the number of pending operations queued for the Greyhole daemon, per share.\n\n";
		printf("%$max_share_strlen"."s  Write   Delete  Rename  Repair (fsck)\n", '');

		foreach ($queues as $share_name => $queue) {
			if ($share_name == 'Spooled') continue;
			if ($share_name == 'Total') {
				for ($i=0; $i<$max_share_strlen; $i++) {
					echo "=";
				}
				echo "\n";
			}
			echo sprintf("%-$max_share_strlen"."s", $share_name) . "  ";
			echo sprintf('%-7s', $queue->num_writes_pending) . " ";
			echo sprintf('%-7s', $queue->num_delete_pending) . " ";
			echo sprintf('%-7s', $queue->num_rename_pending) . " ";
			echo sprintf('%-7s', $queue->num_fsck_pending) . "\n";
		}

		echo "\nThe following is the number of pending operations that the Greyhole daemon still needs to parse.\n";
		echo "Until it does, the nature of those operations is unknown.\n";
		echo "Spooled operations that have been parsed will be listed above and disappear from the count below.\n";

		echo sprintf("\n%-$max_share_strlen"."s  ", 'Spooled');
		echo $queues['Spooled'] . "\n";

		echo "\n";
	}
	exit(0);
}

if ($action == 'status') {
	$last_log_line = exec("tail -1 " . escapeshellarg($greyhole_log_file));
	$last_action_time = strtotime(mb_substr($last_log_line, 0, 15));
	$raw_last_log_line = mb_substr($last_log_line, 16);
	$last_log_line = explode(' ', $raw_last_log_line);
	$last_action = str_replace(':', '', $last_log_line[1]);
	echo "\nLast logged action: $last_action\n";
	echo "  on " . date('Y-m-d H:i:s', $last_action_time) . " (" . how_long_ago($last_action_time) . " ago)\n";
	echo "\nDetails:\n";
	$details = array();
	switch($last_action) {
		case 'balance':
			$details[] = exec("grep 'Balancing storage pool directory' " . escapeshellarg($greyhole_log_file) . " | tail -1");
			$details[] = exec("grep 'Working on file' " . escapeshellarg($greyhole_log_file) . " | tail -1");
			break;
		case 'fsck':
			$details[] = exec("tail -200 " . escapeshellarg($greyhole_log_file) . " | grep -v 'fsck: Found' | tail -1");
			break;
		default:
			$details[] = "... .. ..:..:.. .$raw_last_log_line";
			break;
	}
	foreach ($details as $line) {
		echo "  " . trim(mb_substr($line, 20 + mb_strlen($last_action))) . "\n";
	}
	echo "\n";
	exit(0);
}

if ($action == 'stats') {
	$max_drive_strlen = 0;
	foreach ($storage_pool_directories as $target_drive) {
		if (mb_strlen($target_drive) > $max_drive_strlen) {
			$max_drive_strlen = mb_strlen($target_drive);
		}
	}
	$max_drive_strlen++;

	$totals = array(
		'total_space' => 0,
		'used_space' => 0,
		'free_space' => 0,
		'attic_size' => 0,
		'potential_available_space' => 0
	);

	$dfs = get_free_space_in_storage_pool_dirs();
	$stats = array();
	foreach ($storage_pool_directories as $target_drive) {
		if (!isset($dfs[$target_drive])) {
			$stats[$target_drive] = (object) array();
			continue;
		}

		$df_command = "df -k " . escapeshellarg($target_drive) . " | tail -1";
		unset($responses);
		exec($df_command, $responses);
		
		preg_match("@\s+([0-9]+)\s+([0-9]+)\s+[0-9]+\s+[0-9]+%\s+.+$@", $responses[0], $regs);
		$total_space = (float) $regs[1];
		$used_space = (float) $regs[2];

		$free_space = (float) $dfs[$target_drive];

		$attic_path = clean_dir("$target_drive/.gh_attic");
		if (!file_exists($attic_path)) {
			$attic_size = (float) 0;
		} else {
			$attic_size = (float) trim(exec("du -sk " . escapeshellarg($attic_path) . " | awk '{print $1}'"));
		}

		$potential_available_space = (float) $free_space + $attic_size;
		
		$stats[$target_drive] = (object) array(
			'total_space' => $total_space,
			'used_space' => $used_space,
			'free_space' => $free_space,
			'attic_size' => $attic_size,
			'potential_available_space' => $potential_available_space,
		);

		$totals['total_space'] += $total_space;
		$totals['used_space'] += $used_space;
		$totals['free_space'] += $free_space;
		$totals['attic_size'] += $attic_size;
		$totals['potential_available_space'] += $potential_available_space;
	}
	$stats['Total'] = (object) $totals;
	
	if (isset($options['json'])) {
		echo json_encode($stats);
	} else {
		echo "\nGreyhole Statistics\n===================\n\n";
		echo "Storage Pool\n";
		printf("%$max_drive_strlen"."s    Total -   Used =   Free +  Attic = Possible\n", '');
		foreach ($stats as $target_drive => $stat) {
			if ($target_drive == 'Total') printf("  %-$max_drive_strlen"."s ==========================================\n", "");
			printf("  %-$max_drive_strlen"."s ", "$target_drive:");
			if (empty($stat)) {
				echo "  N/A -   N/A =   N/A +   N/A =   N/A\n";
			} else {
				echo sprintf('%5.0f', $stat->total_space/1024/1024) . "G - " . sprintf('%5.0f', $stat->used_space/1024/1024) . "G = " . sprintf('%5.0f', $stat->free_space/1024/1024) . "G + " . sprintf('%5.0f', $stat->attic_size/1024/1024) . "G = " . sprintf('%5.0f', $stat->potential_available_space/1024/1024) . "G\n";
			}
		}
		echo "\n";
	}

	exit(0);
}

if ($action == 'wait-for') {
	if (!mark_gone_ok($options['dir'])) {
		if (!empty($options['dir'])) {
			echo "Directory " . $options['dir'] . " is not one of your defined storage pool directories.\n";
		}
		echo "Please use one of the following with the --wait-for option:\n  ";
		echo implode("\n  ", $storage_pool_directories) . "\n";
		exit(1);
	}
	gh_log(INFO, "Storage pool directory " . $options['dir'] . " has been marked Temporarily-Gone");
	echo "Storage pool directory " . $options['dir'] . " has been marked Temporarily-Gone, which means the missing file copies that are in this directory will not be re-created until it reappears.\n";
	exit(0);
}

if ($action == 'prerotate') {
	parse_samba_spool(FALSE);
	gh_log(INFO, "Samba log parsed using --prerotate");
	echo "Samba log parsed successfully. You can now rotate the log, then reset the read pointer using --postrotate\n";
	exit(0);
}

if ($action == 'postrotate') {
	$query = sprintf("UPDATE settings SET value = '0' WHERE name = '%s'", 'last_read_log_smbd_line');
	db_query($query) or gh_log(CRITICAL, "Can't update settings for 'last_read_log_smbd_line': " . db_error());
	gh_log(INFO, "Samba log read pointer reset using --postrotate");
	echo "Samba log read pointer reset to 0.\n";
	exit(0);
}

if ($action == 'balance') {
	$query = "INSERT INTO tasks (action, share, complete) VALUES ('balance', '', 'yes')";
	db_query($query) or gh_log(CRITICAL, "Can't insert balance task: " . db_error());
	echo "A balance has been scheduled. It will start after all currently pending tasks have been completed.\n";
	echo "This operation will try to even the available space on all directories included in your storage pool.\n";
	exit(0);
}

if ($action == 'cancel-fsck') {
	db_query("DELETE FROM tasks WHERE action = 'fsck'") or gh_log(CRITICAL, "Can't delete fsck tasks: " . db_error());
	echo "All scheduled fsck tasks have now been deleted.\n";
	if (is_file('/etc/init.d/greyhole')) {
		exec("/etc/init.d/greyhole condrestart");
	} else if (is_file('/etc/init/greyhole.conf')) {
		exec("restart greyhole");
	} else {
		echo "You should now restart the Greyhole daemon to stop any ongoing fsck.\n";
	}
	exit(0);
}

if ($action == 'fsck') {
	$pos = array_search('fsck', $argv);
	$full_path = '';
	if (isset($options['dir'])) {
		$full_path = $options['dir'];
		if (!is_dir($full_path)) {
			echo "$full_path is not a directory. Exiting.\n";
			exit(1);
		}
	}
	$fsck_options = array();
	if (isset($options['email-report'])) {
		$fsck_options[] = 'email';
	}
	if (!isset($options['dont-walk-graveyard'])) {
		$fsck_options[] = 'graveyard';
	}
	if (isset($options['if-conf-changed'])) {
		$fsck_options[] = 'if-conf-changed';
	}
	if (isset($options['disk-usage-report'])) {
		$fsck_options[] = 'du';
	}
	if (isset($options['find-orphaned-files'])){
		$fsck_options[] = 'orphaned';
	}
	$query = sprintf("INSERT INTO tasks (action, share, additional_info, complete) VALUES ('fsck', '%s', %s, 'yes')",
		db_escape_string($full_path),
		(!empty($fsck_options) ? "'" . implode('|', $fsck_options) . "'" : "NULL")
	);
	db_query($query) or gh_log(CRITICAL, "Can't insert fsck task: " . db_error());
	if ($full_path == '') {
		$full_path = 'all shares';
	}
	echo "fsck of $full_path has been scheduled. It will start after all currently pending tasks have been completed.\n";
	exit(0);
}

$num_daemon_processes = exec('ps ax | grep ".*/php .*/greyhole --daemon" | grep -v grep | wc -l');
if ($num_daemon_processes > 1) {
	die("Found an already running Greyhole daemon with PID " . trim(file_get_contents('/var/run/greyhole.pid')) . ".\nCan't start multiple Greyhole daemons.\nQuitting.\n");
}

gh_log(INFO, "Greyhole (version %VERSION%) daemon started.");
set_graveyard_backup();
set_uniq_id();
parse_samba_spool();
simplify_tasks();
while (TRUE) {
	parse_samba_spool();
	$action = 'check_pool';
	check_storage_pool_dirs();
	execute_next_task();
}

function execute_next_task() {
	global $log_level, $fsck_report, $storage_pool_directories, $shares_options, $email_to, $sleep_before_task, $action, $frozen_directories, $next_task, $current_task_id, $locked_files, $result_new_tasks;

	if (isset($next_task)) {
		$task = $next_task;
		$next_task = null;
	} else {
		$task = FALSE;
		if (isset($result_new_tasks)) {
			$task = db_fetch_object($result_new_tasks);
			if ($task === FALSE) {
				db_free_result($result_new_tasks);
			}
		}
		if ($task === FALSE) {
			$query = "SELECT id, action, share, full_path, additional_info, complete FROM tasks WHERE complete IN ('yes', 'thawed') ORDER BY id ASC LIMIT 20";
			$result_new_tasks = db_query($query) or gh_log(CRITICAL, "Can't query tasks: " . db_error());
			$task = db_fetch_object($result_new_tasks);
		}
		if ($task === FALSE) {
			// No more complete = yes|thawed; let's look for complete = idle tasks.
			$query = "SELECT id, action, share, full_path, additional_info, complete FROM tasks WHERE complete = 'idle' ORDER BY id ASC LIMIT 1";
			$result = db_query($query) or gh_log(CRITICAL, "Can't query tasks: " . db_error());
			$task = db_fetch_object($result);
			if ($task === FALSE) {
				$action = 'sleep';
				gh_log(DEBUG, "Nothing to do... Sleeping.");
				sleep($log_level == DEBUG ? 10 : ($log_level == TEST || PERF ? 1 : 600));
				db_free_result($result);
				$locked_files = array();
				return;
			}
		}
	}
	$current_task_id = $task->id;

	# Postpone tasks in frozen directories until a --thaw command is received
	if ($task->complete != 'thawed') {
		foreach ($frozen_directories as $frozen_directory) {
			if (mb_strpos("$task->share/$task->full_path", $frozen_directory) === 0) {
				$action = $task->action;
				gh_log(DEBUG, "Now working on task ID $task->id: $task->action $task->share/$task->full_path" . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : ''));
				gh_log(DEBUG, "  This directory is frozen. Will postpone this task until it is thawed.");
				postpone_task($task->id, 'frozen');
				archive_task($task->id);
				return;
			}
		}
	}
	
	if (($key = array_search($task->id, $sleep_before_task)) !== FALSE) {
		$action = 'sleep';
		gh_log(DEBUG, "Only locked files operations pending... Sleeping.");
		sleep($log_level == DEBUG ? 10 : ($log_level == TEST ? 1 : 600));
		$sleep_before_task = array();
		$locked_files = array();
	}
	
	$action = $task->action;
	gh_log(DEBUG, "Now working on task ID $task->id: $task->action $task->share/$task->full_path" . ($task->action == 'rename' ? " -> $task->share/$task->additional_info" : ''));

	switch ($task->action) {
		case 'balance':
			gh_log(INFO, "Starting available space balancing");
			gh_balance();
			gh_log(INFO, "Available space balancing completed.");
			break;
		case 'fsck_file':
			$task->full_path = get_share_landing_zone($task->share) . '/' . $task->full_path;
			$file_type = @filetype($task->full_path);
			list($path, $filename) = explode_full_path($task->full_path);
			gh_fsck_file($path, $filename, $file_type, 'landing_zone', $task->share);
			break;
		case 'fsck':
			$new_conf_md5 = get_conf_md5();
			if (mb_strpos($task->additional_info, 'if-conf-changed') !== FALSE) {
				// Let's check if the conf file changed since the last fsck

				// Last value
				$last_md5 = '';
				$query = sprintf("SELECT value FROM settings WHERE name = '%s'", 'last_fsck_conf_md5');
				$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for 'last_fsck_conf_md5': " . db_error());
				if ($row = db_fetch_object($result)) {
					$last_md5 = $row->value;
				}
				db_free_result($result);
				
				// New value
				if ($new_conf_md5 == $last_md5) {
					gh_log(INFO, "Skipping fsck; --if-conf-changed was specified, and the configuration file didn't change since the last fsck.");
					break;
				}
			}
			gh_log(INFO, "Starting fsck for $task->share");
			initialize_fsck_report($task->share == '' ? 'All shares' : $task->share);
			if (mb_strpos($task->additional_info, 'du') !== FALSE) {
				global $options;
				$options['disk-usage-report'] = TRUE;
			}
			clearstatcache();
			$orphaned = mb_strpos($task->additional_info, 'orphaned');
			if ($task->share == '') {
				foreach ($shares_options as $share_name => $share_options) {
					gh_fsck($share_options['landing_zone'], $share_name);
				}
				if (mb_strpos($task->additional_info, 'graveyard') !== FALSE) {
					foreach (get_graveyards() as $graveyard) {
						foreach ($shares_options as $share_name => $share_options) {
							gh_fsck_graveyard($graveyard, "/$share_name", $share_name);
						}
					}
				}
				if ($orphaned !== FALSE) {
					foreach ($storage_pool_directories as $target_drive) {
						foreach ($shares_options as $share_name => $share_options) {
							gh_fsck("$target_drive/$share_name", $share_name, $target_drive, TRUE);
						}
					}
				}
			} else {
				$share_options = get_share_options_from_full_path($task->share);
				$storage_volume = FALSE;
				if ($share_options === FALSE){
					//Since share_options is FALSE we didn't get a share path, maybe we got a storage volume path, let's check 
					$storage_volume = get_storage_volume_from_path($task->share);
					$share_options = get_share_options_from_storage_volume($task->share,$storage_volume);
				}
				if ($share_options !== FALSE) {
					$share = $share_options['name'];
					$graveyard = get_graveyard_from_path($task->share);
					if($graveyard === FALSE){
						//Only kick off an fsck on the passed dir if it's not a graveyard
						gh_fsck($task->share, $share, $storage_volume, $orphaned);
					}
					if (mb_strpos($task->additional_info, 'graveyard') !== FALSE) {
						if($graveyard === FALSE){
							//This isn't a graveyard dir so we'll check the graveyard of this path on all volumes
							if($storage_volume !== FALSE){
								$subdir = str_replace($storage_volume, '', $task->share);
							}else{
								$subdir = "/$share" . str_replace($share_options['landing_zone'], '', $task->share);
							}
							gh_log(DEBUG, "Starting graveyards fsck for $subdir");
							foreach (get_graveyards() as $graveyard) {
								gh_fsck_graveyard($graveyard, $subdir, $share);
							}
						}else{
							//This is a graveyard directory, so only kick off a graveyard fsck for the indicated directory (this will not fsck the corresponding graveyard path on other volumes)
							$subdir = str_replace("$graveyard", '', $task->share);
							gh_log(DEBUG, "Starting graveyard fsck for $graveyard/$subdir");
							gh_fsck_graveyard($graveyard,$subdir,$share);
						}
					}
				}
			}
			gh_log(INFO, "fsck for " . ($task->share == '' ? 'All shares' : $task->share) . " completed.");
			
			$query = sprintf("DELETE FROM settings WHERE name = '%s'", 'last_fsck_conf_md5');
			db_query($query) or gh_log(CRITICAL, "Can't delete 'last_fsck_conf_md5' settings: " . db_error());
			$query = sprintf("INSERT INTO settings (name, value) VALUES ('%s', '%s')",
				'last_fsck_conf_md5',
				db_escape_string($new_conf_md5)
			);
			db_query($query) or gh_log(CRITICAL, "Can't insert 'last_fsck_conf_md5' settings: " . db_error());
			
			if (mb_strpos($task->additional_info, 'email') !== FALSE) {
				// Email report for daily fsck
				$fsck_report = get_fsck_report();
				gh_log(DEBUG, "Sending fsck report to $email_to");
				mail($email_to, 'fsck of Greyhole shares on ' . exec('hostname'), $fsck_report);
			}
			if (mb_strpos($task->additional_info, 'du') !== FALSE) {
				// Save disk-usage report to disk
				$fp = fopen('/usr/local/greyhole/gh-disk-usage.log', 'w');
				if ($fp) {
					global $du;
					foreach ($du as $path => $size) {
						$chars_count = count_chars($path, 1);
						fwrite($fp, $chars_count[ord('/')] . " $path $size\n");
					}
					fwrite($fp, "# " . serialize($shares_options) . "\n");
					fclose($fp);
				}
			}
			break;
		case 'mkdir':
			break;
		case 'write':
			gh_write($task->share, $task->full_path, $task->id);
			break;
		case 'rename':
			gh_rename($task->share, $task->full_path, $task->additional_info, $task->id);
			break;
		case 'unlink':
			gh_unlink($task->share, $task->full_path, $task->id);
			break;
		case 'rmdir':
			gh_rmdir($task->share, $task->full_path);
			break;
	}
	if ($task->action != 'write' && $task->action != 'rename') {
		$sleep_before_task = array();
	}
	archive_task($task->id);
}

function archive_task($task_id) {
	$query = sprintf("INSERT INTO tasks_completed SELECT * FROM tasks WHERE id = %d", $task_id);
	$worked = db_query($query);
	if (!$worked) {
		// Let's try a second time... This is kinda important!
		global $db_options;
		db_connect() or gh_log(CRITICAL, "Can't connect to $db_options->engine database.");
		db_query($query) or gh_log(CRITICAL, "Can't insert in tasks_completed: " . db_error());
	}

	$query = sprintf("DELETE FROM tasks WHERE id = %d", $task_id);
	db_query($query) or gh_log(CRITICAL, "Can't delete from tasks: " . db_error());
}

function gh_rmdir($share, $full_path) {
	global $storage_pool_directories, $attic_share_names;
	$landing_zone = get_share_landing_zone($share);
	if (!$landing_zone) {
		return;
	}
	
	gh_log(INFO, "Directory deleted: $landing_zone/$full_path");
	
	if (array_search($share, $attic_share_names) !== FALSE) {
		// Remove that directory from all attics
		foreach ($storage_pool_directories as $target_drive) {
			if (@rmdir("$target_drive/.gh_attic/$full_path")) {
				gh_log(DEBUG, "  Removed copy from attic at $target_drive/.gh_attic/$full_path");
			}
		}
		return;
	}

	foreach ($storage_pool_directories as $target_drive) {
		if (@rmdir("$target_drive/$share/$full_path/")) {
			gh_log(DEBUG, "  Removed copy at $target_drive/$share/$full_path");
		}
		$graveyard = "$target_drive/.gh_graveyard";
		if (@rmdir("$graveyard/$share/$full_path/")) {
			gh_log(DEBUG, "  Removed tombstones directory $graveyard/$share/$full_path");
		}
	}
}

function gh_unlink($share, $full_path, $task_id) {
	global $attic_share_names, $attic_share, $storage_pool_directories;
	
	$landing_zone = get_share_landing_zone($share);
	if (!$landing_zone) {
		return;
	}
	
	gh_log(INFO, "File deleted: $landing_zone/$full_path");
	
	if (array_search($share, $attic_share_names) !== FALSE) {
		// Will delete the file in the attic which has no corresponding symlink in the Greyhole Attic share.
		// That symlink is what was deleted from that share to create the task we're currently working on.
		$full_path = preg_replace('/ copy [0-9]+$/', '', $full_path);
		gh_log(DEBUG, "  Looking for corresponding file in attic to delete...");
		foreach ($storage_pool_directories as $target_drive) {
			if (file_exists("$target_drive/.gh_attic/$full_path")) {
				$delete = TRUE;
				list($path, $filename) = explode_full_path("{$attic_share['landing_zone']}/$full_path");
				if ($dh = opendir($path)) {
					while (($file = readdir($dh)) !== FALSE) {
						if ($file == '.' || $file == '..') { continue; }
						if (is_link("$path/$file") && readlink("$path/$file") == "$target_drive/.gh_attic/$full_path") {
							$delete = FALSE;
							continue;
						}
					}
				}
				if ($delete) {
					gh_log(DEBUG, "    Deleting corresponding copy $target_drive/.gh_attic/$full_path");
					unlink("$target_drive/.gh_attic/$full_path");
					break;
				}
			}
		}
		return;
	}

	if (gh_file_exists("$landing_zone/$full_path") && !is_dir("$landing_zone/$full_path")) {
		gh_log(DEBUG, "  File still exists in landing zone; a new file replaced the one deleted here. Skipping.");
		return;
	}

	list($path, $filename) = explode_full_path($full_path);

	foreach (get_tombstones($share, $path, $filename, TRUE) as $existing_tombstones){
		foreach ($existing_tombstones as $tombstone) {
			gh_recycle($tombstone->path);
		}
	}
	remove_tombstones($share, $path, $filename);
}

function is_a_graveyard_dir($share, $full_path) {
	foreach (get_graveyards() as $graveyard) {
		if (is_dir("$graveyard/$share/$full_path")) {
			return TRUE;
		}
	}
	return FALSE;
}

function gh_rename($share, $full_path, $target_full_path, $task_id) {
	global $storage_pool_directories, $log_level, $sleep_before_task;
	$landing_zone = get_share_landing_zone($share);
	if (!$landing_zone) {
		return;
	}
	
	if (is_dir("$landing_zone/$target_full_path") || is_a_graveyard_dir($share, $full_path)) {
		gh_log(INFO, "Directory renamed: $landing_zone/$full_path -> $landing_zone/$target_full_path");


		foreach ($storage_pool_directories as $target_drive) {
			list($original_path, $dirname) = explode_full_path(get_share_landing_zone($share) . "/$target_full_path");

			if (is_dir("$target_drive/$share/$full_path")) {
				# Make sure the parent directory of target_full_path exists, before we try moving something there...
				list($path, $dirname) = explode_full_path("$target_drive/$share/$target_full_path");
				gh_mkdir($path, $original_path);

				rename("$target_drive/$share/$full_path", "$target_drive/$share/$target_full_path");
				gh_log(DEBUG, "  Directory moved: $target_drive/$share/$full_path -> $target_drive/$share/$target_full_path");
			}

			list($path, $dirname) = explode_full_path("$target_drive/.gh_graveyard/$share/$target_full_path");
			gh_mkdir($path, $original_path);
			$result = @rename("$target_drive/.gh_graveyard/$share/$full_path", "$target_drive/.gh_graveyard/$share/$target_full_path");
			if ($result) {
				gh_log(DEBUG, "  Graveyard directory moved: $target_drive/.gh_graveyard/$share/$full_path -> $target_drive/.gh_graveyard/$share/$target_full_path");
			}
			$result = @rename("$target_drive/.gh_graveyard_backup/$share/$full_path", "$target_drive/.gh_graveyard_backup/$share/$target_full_path");
			if ($result) {
				gh_log(DEBUG, "  Backup graveyard directory moved: $target_drive/.gh_graveyard_backup/$share/$full_path -> $target_drive/.gh_graveyard_backup/$share/$target_full_path");
			}
		}

		foreach (get_tombstones($share, $target_full_path, null, FALSE, FALSE, FALSE) as $existing_tombstones){
			gh_log(DEBUG, "Existing tombstones " . count($existing_tombstones));
			foreach ($existing_tombstones as $file_path => $file_tombstones) {
				gh_log(DEBUG, "file tombstones " . count($file_tombstones));
				$new_file_tombstones = array();
				$symlinked = FALSE;
				foreach ($file_tombstones as $key => $tombstone) {
					$old_path = $tombstone->path;
					$tombstone->path = str_replace("/$share/$full_path/$file_path", "/$share/$target_full_path/$file_path", $tombstone->path);
					gh_log(DEBUG, "  Changing tombstone: $old_path -> $tombstone->path");
					$new_file_tombstones[$tombstone->path] = $tombstone;

					// is_linked = is the target of the existing symlink
					if ($tombstone->is_linked) {
						$symlinked = TRUE;
						$symlink_target = $tombstone->path;
					}
				}
				if (!$symlinked && count($file_tombstones) > 0) {
					// None of the tombstones were is_linked; use the last one for the symlink.
					$tombstone->is_linked = TRUE;
					$file_tombstones[$key] = $tombstone;
					$symlink_target = $tombstone->path;
				}
				
				if (is_link("$landing_zone/$target_full_path/$file_path") && readlink("$landing_zone/$target_full_path/$file_path") != $symlink_target) {
					gh_log(DEBUG, "  Updating symlink at $landing_zone/$target_full_path/$file_path to point to $symlink_target");
					unlink("$landing_zone/$target_full_path/$file_path");
					symlink($symlink_target, "$landing_zone/$target_full_path/$file_path");
				} else if (is_link("$landing_zone/$full_path/$file_path") && !file_exists(readlink("$landing_zone/$full_path/$file_path"))) {
					gh_log(DEBUG, "  Updating symlink at $landing_zone/$full_path/$file_path to point to $symlink_target");
					unlink("$landing_zone/$full_path/$file_path");
					symlink($symlink_target, "$landing_zone/$full_path/$file_path");
				} else {
					fix_symlinks($landing_zone, $share, "$full_path/$file_path", "$target_full_path/$file_path");
				}
				
				list($path, $filename) = explode_full_path("$target_full_path/$file_path");
				save_tombstones($share, $path, $filename, $new_file_tombstones);
			}
		}
	} else {
		gh_log(INFO, "File renamed: $landing_zone/$full_path -> $landing_zone/$target_full_path");
		
		// Check if another process locked this file before we work on it.
		global $locked_files;
		if (isset($locked_files[clean_dir("$share/$target_full_path")]) || file_is_locked($share, $target_full_path) !== FALSE) {
			gh_log(DEBUG, "  File $landing_zone/$target_full_path is locked by another process. Will wait until it's unlocked to work on it.");
			postpone_task($task_id);
			$locked_files[clean_dir("$share/$target_full_path")] = TRUE;
			return;
		}

		list($path, $filename) = explode_full_path($full_path);
		list($target_path, $target_filename) = explode_full_path($target_full_path);

		foreach (get_tombstones($share, $path, $filename, FALSE, FALSE, FALSE) as $existing_tombstones){
			// There might be old tombstones... for example, when a delete task was skipped.
			// Let's remove the file copies if there are any leftovers; correct copies will be re-created below.
			if (file_exists("$landing_zone/$target_full_path") && (count($existing_tombstones) > 0 || !is_link("$landing_zone/$target_full_path"))) {
				foreach (get_tombstones($share, $target_path, $target_filename) as $existing_target_tombstones){
					if (count($existing_target_tombstones) > 0) {
						foreach ($existing_target_tombstones as $tombstone) {
							gh_recycle($tombstone->path);
						}
						remove_tombstones($share, $target_path, $target_filename);
					}
				}
			}
		
			if (count($existing_tombstones) == 0) {
				// Any NOK tombstones that need to be removed?
				foreach (get_tombstones($share, $path, $filename, TRUE, FALSE, FALSE) as $all_existing_tombstones){
					if (count($all_existing_tombstones) > 0) {
						remove_tombstones($share, $path, $filename);
					}
				}
				// New file
				gh_write($share, $target_full_path, $task_id);
			} else {
				$symlinked = FALSE;
				foreach ($existing_tombstones as $key => $tombstone) {
					$old_path = $tombstone->path;
					$tombstone->path = str_replace("/$share/$full_path", "/$share/$target_full_path", $old_path);
					gh_log(DEBUG, "  Renaming copy at $old_path to $tombstone->path");

					// Make sure the target directory exists
					list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);
					list($original_path, $dirname) = explode_full_path(get_share_landing_zone($share) . "/$target_full_path");
					gh_mkdir($tombstone_dir_path, $original_path);

					$it_worked = rename($old_path, $tombstone->path);

					if ($it_worked) {
						// is_linked = is the target of the existing symlink
						if ($tombstone->is_linked) {
							$symlinked = TRUE;
							$symlink_target = $tombstone->path;
						}
					} else {
						gh_log(WARN, "    Warning! An error occured while renaming file copy $old_path to $tombstone->path.");
					}
					$existing_tombstones[$key] = $tombstone;
				}
				if (!$symlinked && count($existing_tombstones) > 0) {
					// None of the tombstones were is_linked; use the last one for the symlink.
					$tombstone->is_linked = TRUE;
					$existing_tombstones[$key] = $tombstone;
					$symlink_target = $tombstone->path;
				}
				remove_tombstones($share, $path, $filename);
				save_tombstones($share, $target_path, $target_filename, $existing_tombstones);

				if (is_link("$landing_zone/$target_full_path")) {
					// New link exists...
					if (readlink("$landing_zone/$target_full_path") != $symlink_target) {
						// ...and needs to be updated.
						gh_log(DEBUG, "  Updating symlink at $landing_zone/$target_full_path to point to $symlink_target");
						unlink("$landing_zone/$target_full_path");
						symlink($symlink_target, "$landing_zone/$target_full_path");
					}
				} else if (is_link("$landing_zone/$full_path") && !file_exists(readlink("$landing_zone/$full_path"))) {
					gh_log(DEBUG, "  Updating symlink at $landing_zone/$full_path to point to $symlink_target");
					unlink("$landing_zone/$full_path");
					symlink($symlink_target, "$landing_zone/$full_path");
				} else {
					fix_symlinks($landing_zone, $share, $full_path, $target_full_path);
				}
			}
		}
	}
	$sleep_before_task = array();
}

function fix_symlinks($landing_zone, $share, $full_path, $target_full_path) {
	global $storage_pool_directories;
	exec("find -L " . escapeshellarg($landing_zone) . " -type l", $broken_links);
	foreach ($broken_links as $broken_link) {
		$fixed_link_target = readlink($broken_link);
		foreach ($storage_pool_directories as $dir) {
			$fixed_link_target = str_replace(clean_dir("$dir/$share/$full_path/"), clean_dir("$dir/$share/$target_full_path/"), $fixed_link_target);
			if ($fixed_link_target == "$dir/$share/$full_path") {
				$fixed_link_target = "$dir/$share/$target_full_path";
				break;
			}
		}
		if (gh_is_file($fixed_link_target)) {
			gh_log(DEBUG, "  Found a broken symlink to update: $broken_link. Old (broken) target: " . readlink($broken_link) . "; new (fixed) target: $fixed_link_target");
			unlink($broken_link);
			symlink($fixed_link_target, $broken_link);
		}
	}
}

function gh_file_exists($real_path, $log_message=null) {
	clearstatcache();
	if (!file_exists($real_path)) {
		if ($log_message != null) {
			eval('$log_message = "' . str_replace('"', '\"', $log_message) . '";');
			gh_log(INFO, $log_message);
		}
		return FALSE;
	}
	return TRUE;
}

function get_num_copies($share) {
	global $shares_options, $storage_pool_directories;
	if (!isset($shares_options[$share]['num_copies'])) {
		global $config_file;
		gh_log(WARNING, "Found a task on a share ($share) that disappeared from $config_file. Skipping.");
		return -1;
	}
	$num_copies = $shares_options[$share]['num_copies'];
	if ($num_copies < 1) {
		$num_copies = 1;
	}
	if ($num_copies > count($storage_pool_directories)) {
		$num_copies = count($storage_pool_directories);
	}
	return $num_copies;
}

function file_is_locked($share, $full_path) {
	global $check_for_open_files;
	if (isset($check_for_open_files) && $check_for_open_files === FALSE) {
		gh_log(DEBUG, "  Skipping open file (lock) check.");
		return FALSE;
	}
	
	$landing_zone = get_share_landing_zone($share);
	if (!$landing_zone) {
		return FALSE;
	}
	
	$real_fullpath = "$landing_zone/$full_path";
	if (is_link($real_fullpath)) {
		$real_fullpath = readlink($real_fullpath);
	}
	$result = exec("lsof -M -n -P -l " . escapeshellarg($real_fullpath) . " 2> /dev/null");
	if (mb_strpos($result, $real_fullpath) !== FALSE) {
		return $result;
	}
	$query = sprintf("SELECT * FROM tasks WHERE complete = 'no' AND action = 'write' AND share = '%s' AND full_path = '%s' LIMIT 1",
		db_escape_string($share),
		db_escape_string($full_path)
	);
	$result = db_query($query) or gh_log(CRITICAL, "Error querying tasks for locked files: " . db_error());
	return db_fetch_object($result) !== FALSE;
}

function real_file_is_locked($real_fullpath) {
	if (is_link($real_fullpath)) {
		$real_fullpath = readlink($real_fullpath);
	}
	$result = exec("lsof -M -n -P -l " . escapeshellarg($real_fullpath) . " 2> /dev/null");
	if (mb_strpos($result, $real_fullpath) !== FALSE) {
		return $result;
	}
	return FALSE;
}

function find_future_full_path($share, $full_path, $task_id) {
	$new_full_path = $full_path;
	while ($next_task = find_next_rename_task($share, $new_full_path, $task_id)) {
		if ($next_task->full_path == $full_path) {
			// File was renamed
			$new_full_path = $next_task->additional_info;
		} else {
			// A parent directory was renamed
			$new_full_path = preg_replace("@^$next_task->full_path@", $next_task->additional_info, $new_full_path);
		}
		$task_id = $next_task->id;
	}
	return $new_full_path;
}

function find_next_rename_task($share, $full_path, $task_id) {
	$full_paths = array();
	$full_paths[] = $full_path;
	$parent_full_path = $full_path;
	list($parent_full_path, $basename) = explode_full_path($parent_full_path);
	while (strlen($parent_full_path) > 1) {
		$full_paths[] = $parent_full_path;
		list($parent_full_path, $basename) = explode_full_path($parent_full_path);
	}
	$query = sprintf("SELECT * FROM tasks WHERE complete = 'yes' AND share = '%s' AND action = 'rename' AND full_path IN ('%s') AND id > %s ORDER BY id LIMIT 1",
		db_escape_string($share),
		implode("','", array_map("db_escape_string", $full_paths)),
		db_escape_string($task_id)
	);
	$result = db_query($query) or gh_log(CRITICAL, "Can't select to find next task: " . db_error());
	return db_fetch_object($result);
}

function gh_write($share, $full_path, $task_id) {
	global $storage_pool_directories, $log_level, $balance_modified_files, $sleep_before_task;
	
	$landing_zone = get_share_landing_zone($share);
	if (!$landing_zone) {
		return;
	}
	
	if (!gh_file_exists("$landing_zone/$full_path", '$real_path doesn\'t exist anymore.')) {
		$new_full_path = find_future_full_path($share, $full_path, $task_id);
		if ($new_full_path != $full_path && gh_is_file("$landing_zone/$new_full_path")) {
			gh_log(DEBUG, "  Found that $full_path has been renamed to $new_full_path. Will work using that instead.");
			if (is_link("$landing_zone/$new_full_path")) {
				$source_file = clean_dir(readlink("$landing_zone/$new_full_path"));
			} else {
				$source_file = clean_dir("$landing_zone/$new_full_path");
			}
		} else {
			gh_log(INFO, "  Skipping.");
			if (!gh_file_exists($landing_zone, '  Share "'.$share.'" landing zone "$real_path" doesn\'t exist anymore. Will not process this task until it re-appears...')) {
				postpone_task($task_id);
			}
			return;
		}
	}
	
	$num_copies_required = get_num_copies($share);
	if ($num_copies_required === -1) {
		return;
	}

	list($path, $filename) = explode_full_path($full_path);

	if ((isset($new_full_path) && is_link("$landing_zone/$new_full_path")) || is_link("$landing_zone/$full_path")) {
		if (!isset($source_file)) {
			$source_file = clean_dir(readlink("$landing_zone/$full_path"));
		}
		gh_log(INFO, "File changed: $share/$full_path", FALSE);
		clearstatcache();
		$filesize = gh_filesize($source_file);
		if ($log_level >= DEBUG) {
			gh_log(DEBUG, " - " . bytes_to_human($filesize, FALSE), FALSE);
		}
		gh_log(INFO, "");
		gh_log(DEBUG, "  Will use source file: $source_file");

		foreach (get_tombstones($share, $path, $filename) as $existing_tombstones){
			gh_log(DEBUG, count($existing_tombstones) . " tombstones loaded.");
			// Will keep existing copies at their location if balance_modified_files = no
			// This will allow us to use rsync instead of blindly copying the complete files.
			if ($balance_modified_files) {
				// Remove old copies (but not the one that was updated!)
				$keys_to_remove = array();
				$found_source_file = FALSE;
				foreach ($existing_tombstones as $key => $tombstone) {
					$tombstone->path = clean_dir($tombstone->path);
					if ($tombstone->path == $source_file) {
						$tombstone->is_linked = TRUE;
						$tombstone->state = 'OK';
						$found_source_file = TRUE;
					} else {
						gh_log(DEBUG, "  Will remove copy at $tombstone->path");
						$keys_to_remove[] = $tombstone->path;
					}
					// This will make sure the correct key is used in the $existing_tombstones array...
					//   This is important because that key is what is used to check if there's already a copy on that drive.
					unset($existing_tombstones[$key]);
					$existing_tombstones[$tombstone->path] = $tombstone;
				}
				if (!$found_source_file) {
					// This shouldn't happen, but if we're about to remove all copies, let's make sure we keep at least one.
					$key = array_shift($keys_to_remove);
					$source_file = $existing_tombstones[$key]->path;
					gh_log(DEBUG, "  Change of mind... Will use source file: $source_file");
				}
				foreach ($keys_to_remove as $key) {
					if ($existing_tombstones[$key]->path != $source_file) {
						gh_recycle($existing_tombstones[$key]->path);
					}
					unset($existing_tombstones[$key]);
				}
			}
			gh_write_process_tombstones($num_copies_required,$existing_tombstones,$share,$full_path,$source_file,$filesize,$task_id);
		}
	} else {
		if (!isset($source_file)) {
			$source_file = clean_dir("$landing_zone/$full_path");
		}
		gh_log(INFO, "File created: $share/$full_path", FALSE);
		clearstatcache();
		$filesize = gh_filesize($source_file);
		if ($log_level >= DEBUG) {
			gh_log(DEBUG, " - " . bytes_to_human($filesize, FALSE), FALSE);
		}
		gh_log(INFO, "");

		if (is_dir($source_file)) {
			gh_log(INFO, "$share/$full_path is now a directory! Aborting.");
			return;
		}

		// There might be old tombstones... for example, when a delete task was skipped.
		// Let's remove the file copies if there are any leftovers; correct copies will be re-created in create_copies_from_tombstones()
		foreach (get_tombstones($share, $path, $filename) as $existing_tombstones){
			gh_log(DEBUG, count($existing_tombstones) . " tombstones loaded.");
			if (count($existing_tombstones) > 0) {
				foreach ($existing_tombstones as $tombstone) {
					gh_recycle($tombstone->path);
				}
				remove_tombstones($share, $path, $filename);
				$existing_tombstones = array();
				// Maybe there's other file copies, that weren't tombstones, or were NOK tombstones!
				global $storage_pool_directories;
				foreach ($storage_pool_directories as $dir) {
					if (file_exists("$dir/$share/$path/$filename")) {
						gh_recycle("$dir/$share/$path/$filename");
					}
				}
			}
			gh_write_process_tombstones($num_copies_required,$existing_tombstones,$share,$full_path,$source_file,$filesize,$task_id);
		}
	}

}

function gh_write_process_tombstones($num_copies_required,$existing_tombstones,$share,$full_path,$source_file,$filesize,$task_id){
	$landing_zone = get_share_landing_zone($share);
	list($path, $filename) = explode_full_path($full_path);

	// Only need to check for locking if we have something to do!
	if ($num_copies_required > 1 || count($existing_tombstones) == 0) {
		// Check if another process locked this file before we work on it.
		global $locked_files;
		if (isset($locked_files[clean_dir("$share/$full_path")]) || ($locked_by = file_is_locked($share, $full_path)) !== FALSE) {
			gh_log(DEBUG, "  File $landing_zone/$full_path is locked by another process. Will wait until it's unlocked to work on it.");
			postpone_task($task_id, 'yes');
			$locked_files[clean_dir("$share/$full_path")] = TRUE;
			return;
		}
		$sleep_before_task = array();
	}
	
	$tombstones = create_tombstones($share, $full_path, $num_copies_required, $filesize, $existing_tombstones);

	if (count($tombstones) == 0) {
		gh_log(WARN, "  No tombstones could be created. Will wait until tombstones can be created to work on this file.");
		postpone_task($task_id);
		return;
	}

	if (!is_link("$landing_zone/$full_path")) {
		// Use the 1st tombstone for the symlink; it might be on a sticky drive.
		$i = 0;
		foreach ($tombstones as $tombstone) {
			$tombstone->is_linked = ($i++ == 0);
		}
	}

	save_tombstones($share, $path, $filename, $tombstones);

	create_copies_from_tombstones($tombstones, $share, $full_path, $source_file);
}

function is_greyhole_owned_dir($path) {
	global $going_dir;
	if (isset($going_dir) && $path == $going_dir) {
		return FALSE;
	}
	return file_exists("$path/.greyhole_uses_this");
}

function create_copies_from_tombstones($tombstones, $share, $full_path, $source_file) {
	$landing_zone = get_share_landing_zone($share);
	
	list($path, $filename) = explode_full_path($full_path);

	$link_next = FALSE;
	$file_infos = gh_get_file_infos("$landing_zone/$full_path");
	foreach ($tombstones as $key => $tombstone) {
		if (!gh_file_exists("$landing_zone/$full_path", '  $real_path doesn\'t exist anymore. Aborting.')) { return; }
		
		if ($tombstone->path == $source_file && $tombstone->state == 'OK' && gh_filesize($tombstone->path) == gh_filesize($source_file)) {
			gh_log(DEBUG, "  File copy at $tombstone->path is already up to date.");
			continue;
		}

		gh_log(DEBUG, "  Copying file to $tombstone->path");

		$root_path = str_replace(clean_dir("/$share/$full_path"), '', $tombstone->path);
		if (!is_greyhole_owned_dir($root_path)) {
			gh_log(WARN, "  Warning! It seems $root_path is missing it's \".greyhole_uses_this\" file. This either means this mount is currently unmounted, or you forgot to create this file.");
			$tombstone->state = 'Gone';
			$tombstones[$key] = $tombstone;
			continue;
		}

		list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);

		list($original_path, $tombstone_filename) = explode_full_path(get_share_landing_zone($share) . "/$full_path");
		if (!gh_mkdir($tombstone_dir_path, $original_path)) {
			$tombstone->state = 'Gone';
			$tombstones[$key] = $tombstone;
			continue;
		}

		$temp_path = get_temp_filename($tombstone->path);
		
		$it_worked = FALSE;
		if (is_link("$landing_zone/$full_path")) {
			exec("rsync -t " . escapeshellarg(readlink("$landing_zone/$full_path")) . " " . escapeshellarg($temp_path));
			$it_worked = file_exists($temp_path) && gh_filesize($temp_path) == gh_filesize(readlink("$landing_zone/$full_path"));
		} else if (gh_is_file("$landing_zone/$full_path")) {
			$source_size = gh_filesize("$landing_zone/$full_path");
			$source_dev = gh_file_deviceid("$landing_zone/$full_path");
			$target_dev = gh_file_deviceid($tombstone_dir_path);
			if ($source_dev === $target_dev && $source_dev !== FALSE) {
				gh_log(DEBUG, "  (using rename)");
				rename("$landing_zone/$full_path", $temp_path);
				$copied = FALSE;
			} else {
				exec("rsync -t " . escapeshellarg("$landing_zone/$full_path") . " " . escapeshellarg($temp_path));
				$copied = TRUE;
			}
			$it_worked = file_exists($temp_path) && gh_filesize($temp_path) == $source_size;
			if ($copied && $it_worked && file_exists("$landing_zone/$full_path")) {
				unlink("$landing_zone/$full_path");
			}
		}
		
		if ($it_worked) {
			rename($temp_path, $tombstone->path);
			gh_chperm($tombstone->path, $file_infos);
		} else {
			gh_log(WARN, "    Failed file copy. Will mark this tombstone 'Gone'.");
			@unlink($temp_path);
			if ($tombstone->is_linked) {
				$tombstone->is_linked = FALSE;
				$link_next = TRUE;
			}
			$tombstone->state = 'Gone';
			gh_recycle($tombstone->path);
			$tombstones[$key] = $tombstone;
			save_tombstones($share, $path, $filename, array_values($tombstones));

			if (file_exists("$landing_zone/$full_path")) {
				global $current_task_id;
				if ($current_task_id === 0) {
					gh_log(ERROR, "    Failed file copy (cont). We already retried this task. Aborting.");
					return;
				}
				gh_log(WARN, "    Failed file copy (cont). Will try to re-process this write task, since the source file seems intact.");
				// Queue a new write task, to replace the now gone copy.
				global $next_task;
				$next_task = (object) array(
					'id' => 0,
					'action' => 'write',
					'share' => $share, 
					'full_path' => clean_dir($full_path),
					'complete' => 'yes'
				);
				return;
			}
		    continue;
		}

		if ($link_next && !$tombstone->is_linked) {
			$tombstone->is_linked = TRUE;
			$tombstones[$key] = $tombstone;
		}
		$link_next = FALSE;
		if ($tombstone->is_linked) {
			gh_log(DEBUG, "    Creating symlink in share pointing to the above file copy.");
			symlink($tombstone->path, "$landing_zone/$path/.gh_$filename");
			if (!file_exists("$landing_zone/$full_path") || unlink("$landing_zone/$full_path")) {
				rename("$landing_zone/$path/.gh_$filename", "$landing_zone/$path/$filename");
			} else {
				unlink("$landing_zone/$path/.gh_$filename");
			}
		}

		if (gh_file_exists($tombstone->path, '  Copy at $real_path doesn\'t exist. Will not mark it OK!')) {
			$tombstone->state = 'OK';
			$tombstones[$key] = $tombstone;
		}
		save_tombstones($share, $path, $filename, array_values($tombstones), get_storage_volume_from_path($tombstone->path));
	}
}

function gh_chperm($real_file_path, $file_infos) {
	chmod($real_file_path, $file_infos->fileperms);
	chown($real_file_path, $file_infos->fileowner);
	chgrp($real_file_path, $file_infos->filegroup);
}

function gh_mkdir($directory, $original_directory_or_dir_infos) {
	if (!is_dir($directory)) {
		if (is_string($original_directory_or_dir_infos)) {
			$dir_infos = gh_get_file_infos($original_directory_or_dir_infos);
		} else {
			$dir_infos = $original_directory_or_dir_infos;
		}
		
		// Need to mkdir & chown/chgrp all dirs that don't exists, up to the full path ($directory)
		$dir_parts = explode('/', $directory);
		
		$i = 0;
		$parent_directory = clean_dir('/' . $dir_parts[$i++]);
		while (is_dir($parent_directory)) {
			$parent_directory = clean_dir($parent_directory . '/' . $dir_parts[$i++]);
		}
		while ($i <= count($dir_parts)) {
			if (!is_dir($parent_directory) && !mkdir($parent_directory, $dir_infos->fileperms)) {
				if (gh_is_file($parent_directory)) {
					rename($parent_directory, "$parent_directory (file copy)");
				}
				if (!mkdir($parent_directory, $dir_infos->fileperms)) {
					gh_log(WARN, "  Failed to create directory $parent_directory");
					return FALSE;
				}
			}
			if (!chown($parent_directory, $dir_infos->fileowner)) {
				gh_log(WARN, "  Failed to chown directory '$parent_directory'");
			}
			if (!chgrp($parent_directory, $dir_infos->filegroup)) {
				gh_log(WARN, "  Failed to chgrp directory '$parent_directory'");
			}
			if (!isset($dir_parts[$i])) {
				break;
			}
			$parent_directory = clean_dir($parent_directory . '/' . $dir_parts[$i++]);
		}
	}
	return TRUE;
}

function get_temp_filename($full_path) {
	list($path, $filename) = explode_full_path($full_path);
	return "$path/.$filename." . mb_substr(md5($filename), 0, 5);
}

function create_tombstones($share, $full_path, $num_copies_required, $filesize, $tombstones=array()) {
	$found_link_tombstone = FALSE;

	list($path, $filename) = explode_full_path($full_path);
	
	$num_ok = count($tombstones);
	foreach ($tombstones as $key => $tombstone) {
		if (!file_exists($tombstone->path)) {
			$tombstone->state = 'Gone';
		}
		$root_path = str_replace(clean_dir("/$share/$full_path"), '', $tombstone->path);
		if (!is_greyhole_owned_dir($root_path)) {
			$tombstone->state = 'Gone';
		}
		if ($tombstone->state != 'OK' && $tombstone->state != 'Pending') {
			$num_ok--;
		}
		if ($key != $tombstone->path) {
			unset($tombstones[$key]);
			$key = $tombstone->path;
		}
		if ($tombstone->is_linked) {
			$found_link_tombstone = TRUE;
		}
		$tombstones[$key] = $tombstone;
	}

	// Select drives that have enough free space for this file
	if ($num_ok < $num_copies_required) {
		$local_target_drives = order_target_drives($filesize/1024, FALSE, $share, $path, '  ');
	}
	while ($num_ok < $num_copies_required && count($local_target_drives) > 0) {
		$target_drive = array_shift($local_target_drives);
		$clean_target_full_path = clean_dir("$target_drive/$share/$full_path");
		// Don't use drives that already have a copy
		if (isset($tombstones[$clean_target_full_path])) {
			continue;
		}
		foreach ($tombstones as $tombstone) {
			if ($clean_target_full_path == clean_dir($tombstone->path)) {
				continue;
			}
		}
		// Prepend new target drives, to make sure sticky directories will be used first
		$tombstones = array_reverse($tombstones);
		$tombstones[$clean_target_full_path] = (object) array('path' => $clean_target_full_path, 'is_linked' => FALSE, 'state' => 'Pending');
		$tombstones = array_reverse($tombstones);
		$num_ok++;
	}
	
	if (!$found_link_tombstone) {
		foreach ($tombstones as $tombstone) {
			$tombstone->is_linked = TRUE;
			break;
		}
	}
	
	return $tombstones;
}

function get_tombstone_data_filename($share, $path, $filename) {
	$filenames = get_tombstone_data_filenames($share, $path, $filename, 1);
	if (count($filenames) > 0) {
		return $filenames[0];
	}
	return FALSE;
}

function get_tombstone_data_filenames($share, $path, $filename, $how_many=9999) {
	$filenames = array();
	foreach (get_graveyards() as $graveyard) {
		$f = "$graveyard/$share/$path/$filename";
		if (is_file($f)) {
			$filenames[] = $f;
			if (count($filenames) == $how_many) {
				return $filenames;
			}
		}
	}
	return $filenames;
}

function get_tombstones($share, $path, $filename=null, $load_nok_tombstones=FALSE, $quiet=FALSE, $check_symlink=TRUE){
	if($filename === null){
		return new tombstone_iterator($share, $path, $load_nok_tombstones, $quiet, $check_symlink);
	}else{
		return array(get_tombstones_for_file($share,$path,$filename,$load_nok_tombstones,$quiet,$check_symlink));
	}	
}

function get_tombstones_for_file($share, $path, $filename=null, $load_nok_tombstones=FALSE, $quiet=FALSE, $check_symlink=TRUE) {
	if (!$quiet) {
		gh_log(DEBUG, "Loading tombstones for $share" . (!empty($path) ? "/$path" : "") . "/$filename... ", FALSE);
	}
	$tombstones_data_file = get_tombstone_data_filename($share, $path, $filename);
	clearstatcache();
	$tombstones = array();
	if (file_exists($tombstones_data_file)) {
		$t = file_get_contents($tombstones_data_file);
		$tombstones = unserialize($t);
	}

	if ($check_symlink) {
		// Fix wrong 'is_linked' flags
		$share_file = get_share_landing_zone($share) . "/$path/$filename";
		$share_file_link_to = FALSE;
		if (is_link($share_file)) {
			$share_file_link_to = readlink($share_file);
		}
		foreach ($tombstones as $key => $tombstone) {
			if ($tombstone->state == 'OK' && $share_file_link_to !== FALSE) {
				if ($tombstone->is_linked && $tombstone->path != $share_file_link_to) {
					if (!$quiet) {
						gh_log(DEBUG, 'Changing is_linked to FALSE for ' . $tombstone->path);
					}
					$tombstone->is_linked = FALSE;
					$tombstones[$key] = $tombstone;
					save_tombstones($share, $path, $filename, array_values($tombstones));
				} else if (!$tombstone->is_linked && $tombstone->path == $share_file_link_to) {
					if (!$quiet) {
						gh_log(DEBUG, 'Changing is_linked to TRUE for ' . $tombstone->path);
					}
					$tombstone->is_linked = TRUE;
					$tombstones[$key] = $tombstone;
					save_tombstones($share, $path, $filename, array_values($tombstones));
				}
			}
		}
	}

	$ok_tombstones = array();
	foreach ($tombstones as $key => $tombstone) {
		$valid_path = FALSE;
		
		$dir = get_storage_volume_from_path($tombstone->path);
		if ($dir !== FALSE) {
			$valid_path = TRUE;
		}
		if ($valid_path && ($load_nok_tombstones || $tombstone->state == 'OK')) {
			$ok_tombstones[$key] = $tombstone;
		} else {
			if (!$valid_path) {
				gh_log(INFO, "Found a tombstone pointing to a directory not defined in your storage pool: '$tombstone->path'. Will mark it as Gone.");
				$tombstone->state = 'Gone';
				$tombstones[$key] = $tombstone;
				save_tombstones($share, $path, $filename, array_values($tombstones));
			} else {
				gh_log(INFO, "Found a tombstone, pointing to '$tombstone->path', with state = '$tombstone->state'. We just want 'OK' tombstones; will not use this tombstone.");
			}
		}
	}
	$tombstones = $ok_tombstones;
	
	if (!$quiet) {
		gh_log(DEBUG, "Got " . count($tombstones) . " tombstones.");
	}
	return $tombstones;
}

function remove_tombstones($share, $path, $filename) {
	gh_log(DEBUG, "  Removing tombstones for $share" . (!empty($path) ? "/$path" : "") . ($filename!== null ? "/$filename" : ""));
	
	foreach (get_tombstone_data_filenames($share, $path, $filename) as $f) {
		@unlink($f);
		gh_log(DEBUG, "    Removed tombstone at $f");
		clearstatcache();
	}
}

function save_tombstones($share, $path, $filename, $tombstones, $only_target_drive=NULL) {
	if (count($tombstones) == 0) {
		remove_tombstones($share, $path, $filename);
		return;
	}

	gh_log(DEBUG, "  Saving " . (empty($only_target_drive) ? count($tombstones) : 1) . " tombstones for $share" . (!empty($path) ? "/$path" : "") . ($filename!== null ? "/$filename" : ""));
	$paths_used = array();
	foreach (get_graveyards() as $graveyard) {
		$target_drive = str_replace('/.gh_graveyard', '', $graveyard);
		if (!empty($only_target_drive) && $only_target_drive != $target_drive) {
			continue;
		}
		$data_filepath = "$graveyard/$share/$path";
		$has_tombstone = FALSE;
		foreach ($tombstones as $tombstone) {
			if (get_storage_volume_from_path($tombstone->path) == $target_drive) {
				gh_mkdir($data_filepath, get_share_landing_zone($share) . "/$path");
				gh_log(DEBUG, "    Saving tombstones in $data_filepath/$filename");
				if (is_dir("$data_filepath/$filename")) {
					exec("rm -rf " . escapeshellarg("$data_filepath/$filename"));
				}
				file_put_contents("$data_filepath/$filename", serialize($tombstones));
				$has_tombstone = TRUE;
				$paths_used[] = $data_filepath;
				break;
			}
		}
		if (!$has_tombstone && file_exists("$data_filepath/$filename")) {
			unlink("$data_filepath/$filename");
		}
	}
	if (count($paths_used) == 1 && empty($only_target_drive)) {
		// Also save a backup on another drive
		global $graveyard_backup_directories;
		if (count($graveyard_backup_directories) > 0) {
			if (mb_strpos($paths_used[0], str_replace('.gh_graveyard_backup', '.gh_graveyard', $graveyard_backup_directories[0])) === FALSE) {
				$graveyard_backup_directory = $graveyard_backup_directories[0];
			} else {
				$graveyard_backup_directory = $graveyard_backup_directories[1];
			}
			$data_filepath = "$graveyard_backup_directory/$share/$path";
			gh_log(DEBUG, "    Saving tombstones (backup) in $data_filepath/$filename");
			gh_mkdir($data_filepath, get_share_landing_zone($share) . "/$path");
			file_put_contents("$data_filepath/$filename", serialize($tombstones));
		}
	}
}

function simplify_tasks() {
	global $action;
	$action = 'simplify_tasks';
	
	gh_log(DEBUG, "Simplifying pending tasks.");
	
	// Remove locked write tasks
	$query = "SELECT share, full_path FROM tasks WHERE action = 'write' and complete = 'no'";
	$result = db_query($query) or gh_log(CRITICAL, "Can't select locked write tasks: " . db_error());
	while ($row = db_fetch_object($result)) {
		$query = sprintf("DELETE FROM tasks WHERE action = 'write' and complete = 'yes' AND share = '%s' AND full_path = '%s'",
			db_escape_string($row->share),
			db_escape_string($row->full_path)
		);
		db_query($query) or gh_log(CRITICAL, "Can't delete duplicate of locked write task: " . db_error());
	}
}

function parse_samba_log($simplify_after_parse=TRUE) {
	global $samba_log_file, $action, $attic_share_names, $max_queued_tasks;

	if (empty($samba_log_file)) {
		return;
	}

	$old_action = $action;
	$action = 'parse_logs';

	// If we have enough queued tasks (90% of $max_queued_tasks), let's not parse the log at this time, and get some work done.
	// Once we fall below that, we'll queue up to at most $max_queued_tasks new tasks, then get back to work.
	// This will effectively 'batch' large file operations to make sure the DB doesn't become a problem because of the number of rows,
	//   and this will allow the end-user to see real activity, other that new rows in greyhole.tasks...
	$query = "SELECT COUNT(*) num_rows FROM tasks";
	$result = db_query($query) or gh_log(CRITICAL, "Can't get tasks count: " . db_error());
	$row = db_fetch_object($result);
	db_free_result($result);
	$num_rows = (int) $row->num_rows;
	if ($num_rows >= ($max_queued_tasks * 0.9)) {
		$action = $old_action;
		gh_log(DEBUG, "  More than " . ($max_queued_tasks * 0.9) . " tasks queued... Won't queue any more at this time.");
		return;
	}

	$query = sprintf("SELECT value FROM settings WHERE name = '%s'", 'last_read_log_smbd_line');
	$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for 'last_read_log_smbd_line': " . db_error());
	$row = db_fetch_object($result);
	if ($row === FALSE || db_fetch_object($result) !== FALSE) {
		gh_log(WARN, "Received " . ($row === FALSE ? 'no rows' : 'more than one row') . " when querying settings for 'last_read_log_smbd_line'; expected one.");
		$query = "INSERT INTO settings (name, value) VALUES ('last_read_log_smbd_line', '0')";
		db_query($query) or gh_log(CRITICAL, "Can't insert new settings for last_read_log_smbd_line: " . db_error());
		$row = (object) array('value' => 0);
	}
	db_free_result($result);

	$f_seek_point = (int) $row->value;

	clearstatcache();
	$samba_log_size = gh_filesize($samba_log_file);
	if ($f_seek_point > $samba_log_size) {
		gh_log(DEBUG, "Log file size = $samba_log_size; forcing seek point to 0.");
		$f_seek_point = 0;
	}

	$fp = fopen($samba_log_file, 'r') or gh_log(CRITICAL, "Can't open Samba log file '$samba_log_file' for reading.");
	fseek($fp, $f_seek_point);

	$new_tasks = 0;
	$last_line = FALSE;
	$act = FALSE;
	while ($line = fgets($fp)) {
		if ($last_line === FALSE) {
			gh_log(DEBUG, "Parsing Samba logs...");
		}
		if (($p = mb_strpos($line, ' smbd_greyhole:')) === FALSE) {
			$last_line = 0;
		} else {
			$line = trim(mb_substr($line, $p+16));

			// Prevent insertion of unneeded duplicates
			if ($line === $last_line) {
				continue;
			}

			$line_ar = explode('*', $line);
			if (count($line_ar) == 1) {
				continue;
			}

			$last_line = $line;

			// Close logs are only processed when no more duplicates are found, so we'll execute this now that a non-duplicate line was found.
			if ($act === 'close') {
				$query = sprintf("UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'no' AND share = '%s' AND additional_info = '%s'",
					db_escape_string($share),
					$fd
				);
				db_query($query) or gh_log(CRITICAL, "Error updating tasks (1): " . db_error() . "; Query: $query");
			}

			$line = $line_ar;
			$act = array_shift($line);
			$share = array_shift($line);
			if ($act == 'mkdir') {
				// Nothing to do with those
				continue;
			}
			$result = array_pop($line);
			if (mb_strpos($result, 'failed') === 0) {
				gh_log(DEBUG, "Failed $act in $share/$line[0]. Skipping.");
				continue;
			}
			unset($fullpath);
			unset($fullpath_target);
			unset($fd);
			switch ($act) {
				case 'open':
					$fullpath = array_shift($line);
					$fd = array_shift($line);
					$act = 'write';
					break;
				case 'rmdir':
				case 'unlink':
					$fullpath = array_shift($line);
					break;
				case 'rename':
					$fullpath = array_shift($line);
					$fullpath_target = array_shift($line);
					break;
				case 'close':
					$fd = array_shift($line);
					break;
				default:
					$act = FALSE;
			}
			if ($act === FALSE) {
				continue;
			}

			// Close logs are only processed when no more duplicates are found, so we won't execute it just yet; we'll process it the next time we find a non-duplicate line.
			if ($act != 'close') {
				if (isset($fd) && $fd == -1) {
					continue;
				}
				if ($act != 'unlink' && $act != 'rmdir' && array_search($share, $attic_share_names) !== FALSE) { continue; }
				$new_tasks++;
				$query = sprintf("INSERT INTO tasks (action, share, full_path, additional_info, complete) VALUES ('%s', '%s', %s, %s, '%s')",
					$act,
					db_escape_string($share),
					isset($fullpath) ? "'".db_escape_string(clean_dir($fullpath))."'" : 'NULL',
					isset($fullpath_target) ? "'".db_escape_string(clean_dir($fullpath_target))."'" : (isset($fd) ? "'$fd'" : 'NULL'),
					$act == 'write' ? 'no' : 'yes'
				);
				db_query($query) or gh_log(CRITICAL, "Error inserting task: " . db_error() . "; Query: $query");
			}

			update_last_read_log_smbd_line($fp);
			
			// If we have enough queued tasks ($max_queued_tasks), let's stop parsing the log, and get some work done.
			if ($num_rows+$new_tasks >= $max_queued_tasks) {
				gh_log(DEBUG, "  We now have more than $max_queued_tasks tasks queued... Will stop parsing for now.");
				break;
			}
		}
	}
	update_last_read_log_smbd_line($fp);
	fclose($fp);

	// Close logs are only processed when no more duplicates are found, so we'll execute this now that we're done parsing the current log.
	if ($act == 'close') {
		$query = sprintf("UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'no' AND share = '%s' AND additional_info = '%s'",
			db_escape_string($share),
			$fd
		);
		db_query($query) or gh_log(CRITICAL, "Error updating tasks (2): " . db_error() . "; Query: $query");
	}
	if ($last_line !== FALSE) {
		gh_log(DEBUG, "  Done parsing.");
	}
	
	if ($simplify_after_parse && $new_tasks > 0) {
		$query = "SELECT COUNT(*) num_rows FROM tasks";
		$result = db_query($query) or gh_log(CRITICAL, "Can't get tasks count: " . db_error());
		$row = db_fetch_object($result);
		db_free_result($result);
		$num_rows = (int) $row->num_rows;
		if ($num_rows < 1000 || $num_rows % 5 == 0) { // Runs 1/5 of the times when num_rows > 1000
			if ($num_rows < 5000 || $num_rows % 100 == 0) { // Runs 1/100 of the times when num_rows > 5000
				simplify_tasks();
			}
		}
	}
	$action = $old_action;
}

function update_last_read_log_smbd_line($fp) {
	$f_seek_point = ftell($fp);
	$query = sprintf("UPDATE settings SET value = '%d' WHERE name = '%s'", $f_seek_point, 'last_read_log_smbd_line');
	db_query($query) or gh_log(CRITICAL, "Can't update settings for 'last_read_log_smbd_line': " . db_error());
}

function parse_samba_spool($simplify_after_parse=TRUE) {
	global $action, $attic_share_names, $max_queued_tasks, $db_use_mysql;

	// Let's parse syslog still... just in case people are still using that.
	parse_samba_log($simplify_after_parse);

	$old_action = $action;
	$action = 'read_smb_spool';

	// If we have enough queued tasks (90% of $max_queued_tasks), let's not parse the log at this time, and get some work done.
	// Once we fall below that, we'll queue up to at most $max_queued_tasks new tasks, then get back to work.
	// This will effectively 'batch' large file operations to make sure the DB doesn't become a problem because of the number of rows,
	//   and this will allow the end-user to see real activity, other that new rows in greyhole.tasks...
	$query = "SELECT COUNT(*) num_rows FROM tasks";
	$result = db_query($query) or gh_log(CRITICAL, "Can't get tasks count: " . db_error());
	$row = db_fetch_object($result);
	db_free_result($result);
	$num_rows = (int) $row->num_rows;
	if ($num_rows >= ($max_queued_tasks * 0.9)) {
		$action = $old_action;
		gh_log(DEBUG, "  More than " . ($max_queued_tasks * 0.9) . " tasks queued... Won't queue any more at this time.");
		return;
	}

	$new_tasks = 0;
	$last_line = FALSE;
	$act = FALSE;
	while (TRUE) {
		$files = array();
		exec('ls -1 /var/spool/greyhole | sort -n 2> /dev/null | head -1000', $files);
		if (count($files) == 0) {
			break;
		}

		if ($last_line === FALSE) {
			gh_log(DEBUG, "Processing Samba spool...");
		}
		
		foreach ($files as $filename) {
			@unlink($last_filename);

			$filename = "/var/spool/greyhole/$filename";
			$last_filename = $filename;

			$line = file_get_contents($filename);

			// Prevent insertion of unneeded duplicates
			if ($line === $last_line) {
				continue;
			}

			$line_ar = explode("\n", $line);

			$last_line = $line;

			// Close logs are only processed when no more duplicates are found, so we'll execute this now that a non-duplicate line was found.
			if ($act === 'close') {
				$query = sprintf("UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'no' AND share = '%s' AND additional_info = '%s'",
					db_escape_string($share),
					$fd
				);
				db_query($query) or gh_log(CRITICAL, "Error updating tasks (1): " . db_error() . "; Query: $query");
			}

			$line = $line_ar;
			$act = array_shift($line);
			$share = array_shift($line);
			if ($act == 'mkdir') {
				// Nothing to do with those
				continue;
			}
			$result = array_pop($line);
			if (mb_strpos($result, 'failed') === 0) {
				gh_log(DEBUG, "Failed $act in $share/$line[0]. Skipping.");
				continue;
			}
			unset($fullpath);
			unset($fullpath_target);
			unset($fd);
			switch ($act) {
				case 'open':
					$fullpath = array_shift($line);
					$fd = array_shift($line);
					$act = 'write';
					break;
				case 'rmdir':
				case 'unlink':
					$fullpath = array_shift($line);
					break;
				case 'rename':
					$fullpath = array_shift($line);
					$fullpath_target = array_shift($line);
					break;
				case 'close':
					$fd = array_shift($line);
					break;
				default:
					$act = FALSE;
			}
			if ($act === FALSE) {
				continue;
			}

			// Close logs are only processed when no more duplicates are found, so we won't execute it just yet; we'll process it the next time we find a non-duplicate line.
			if ($act != 'close') {
				if (isset($fd) && $fd == -1) {
					continue;
				}
				if ($act != 'unlink' && $act != 'rmdir' && array_search($share, $attic_share_names) !== FALSE) { continue; }
				$new_tasks++;
				$query = sprintf("INSERT INTO tasks (action, share, full_path, additional_info, complete) VALUES ('%s', '%s', %s, %s, '%s')",
					$act,
					db_escape_string($share),
					isset($fullpath) ? "'".db_escape_string(clean_dir($fullpath))."'" : 'NULL',
					isset($fullpath_target) ? "'".db_escape_string(clean_dir($fullpath_target))."'" : (isset($fd) ? "'$fd'" : 'NULL'),
					$act == 'write' ? 'no' : 'yes'
				);
				db_query($query) or gh_log(CRITICAL, "Error inserting task: " . db_error() . "; Query: $query");
			}

			// If we have enough queued tasks ($max_queued_tasks), let's stop parsing the log, and get some work done.
			if ($num_rows+$new_tasks >= $max_queued_tasks) {
				gh_log(DEBUG, "  We now have more than $max_queued_tasks tasks queued... Will stop parsing for now.");
				break;
			}
		}
		@unlink($last_filename);
		if ($num_rows+$new_tasks >= $max_queued_tasks) {
			break;
		}
	}

	// Close logs are only processed when no more duplicates are found, so we'll execute this now that we're done parsing the current log.
	if ($act == 'close') {
		$query = sprintf("UPDATE tasks SET additional_info = NULL, complete = 'yes' WHERE complete = 'no' AND share = '%s' AND additional_info = '%s'",
			db_escape_string($share),
			$fd
		);
		db_query($query) or gh_log(CRITICAL, "Error updating tasks (2): " . db_error() . "; Query: $query");
	}
	
	if ($new_tasks > 0) {
		gh_log(DEBUG, "Found $new_tasks new tasks in spool.");

		if ($simplify_after_parse) {
			$query = "SELECT COUNT(*) num_rows FROM tasks";
			$result = db_query($query) or gh_log(CRITICAL, "Can't get tasks count: " . db_error());
			$row = db_fetch_object($result);
			db_free_result($result);
			$num_rows = (int) $row->num_rows;
			if ($num_rows < 1000 || $num_rows % 5 == 0) { // Runs 1/5 of the times when num_rows > 1000
				if ($num_rows < 5000 || $num_rows % 100 == 0) { // Runs 1/100 of the times when num_rows > 5000
					simplify_tasks();
				}
			}
		}
	}
	
	$action = $old_action;
}

function get_free_space_in_storage_pool_dirs() {
	global $storage_pool_directories, $df_command, $last_df_time, $last_dfs, $df_cache_time;
	if ($last_df_time > time() - $df_cache_time) {
		return $last_dfs;
	}
	$dfs = array();
	exec($df_command, $responses);
	$responses_arr = array();
	foreach ($responses as $line) {
		if (preg_match("@\s+[0-9]+\s+[0-9]+\s+([0-9]+)\s+[0-9]+%\s+(.+)$@", $line, $regs)) {
			$responses_arr[] = "$regs[2] $regs[1]";
		}
	}
	$responses = $responses_arr;
	foreach ($storage_pool_directories as $target_drive) {
		$target_dir = '';
		for ($i=0; $i<count($responses); $i++) {
			$response = explode(' ', $responses[$i]);
			$free_space = (float) array_pop($response);
			$mount = implode(' ', $response);
			if (mb_strpos($target_drive, $mount) === 0 && mb_strlen($mount) > mb_strlen($target_dir)) {
				$target_dir = $mount;
				$target_freespace = $free_space;
			}
		}
		$dfs[$target_drive] = $target_freespace;
	}
	$last_df_time = time();
	$last_dfs = $dfs;
	return $dfs;
}

function order_target_drives($filesize_kb, $include_full_drives, $share, $path, $log_prefix='') {
	global $storage_pool_directories, $minimum_free_space_pool_directories, $dir_selection_algorithm, $last_OOS_notification, $sticky_files;
	$sorted_target_drives = array();
	$last_resort_sorted_target_drives = array();
	$full_drives = array();

	$dfs = get_free_space_in_storage_pool_dirs();

	foreach ($storage_pool_directories as $target_drive) {
		if (!isset($dfs[$target_drive])) {
			if (!is_dir($target_drive)) {
				gh_log(ERROR, "The directory at $target_drive doesn't exist. This partition will never be used!");
				if (file_exists('/usr/bin/hdactl')) {
					gh_log(ERROR, "You should de-select, then re-select this partition in your Amahi dashboard (http://hda), in the Shares > Storage Pool page, to fix this problem.");
				} else {
					gh_log(ERROR, "See the INSTALL file for instructions on how to prepare partitions to include in your storage pool.");
				}
			} else {
				gh_log(ERROR, "Can't find how much free space is left on $target_drive. This partition will never be used!");
				gh_log(ERROR, "Please report this using the 'Bugs Tracker' link found on http://greyhole.net. You should include the following information in your ticket:");
				gh_log(ERROR, "===== Error report starts here =====");
				gh_log(ERROR, "Unknown free space for partition: $target_drive");
				global $df_command;
				gh_log(ERROR, "df_command: $df_command");
				unset($responses);
				exec($df_command, $responses);
				gh_log(ERROR, "Result of df_command: " . var_export($responses, TRUE));
				unset($responses);
				exec('df -k 2>&1', $responses);
				gh_log(ERROR, "Result of df -k: " . var_export($responses, TRUE));
				gh_log(ERROR, "===== Error report ends here =====");
			}
			continue;
		}
		if (!is_greyhole_owned_dir($target_drive)) {
			continue;
		}
		$free_space = $dfs[$target_drive];
		$minimum_free_space = (float) (isset($minimum_free_space_pool_directories[$target_drive]) ? $minimum_free_space_pool_directories[$target_drive]*1024*1024 : 0.0);
		$available_space = (float) $free_space - $minimum_free_space;
		if ($available_space <= $filesize_kb) {
			if ($free_space > $filesize_kb) {
				while (isset($last_resort_sorted_target_drives[$free_space])) {
					$free_space -= 0.001;
				}
				$last_resort_sorted_target_drives[$available_space] = $target_drive;
			} else {
				while (isset($full_drives[$free_space])) {
					$free_space -= 0.001;
				}
				$full_drives[$free_space] = $target_drive;
			}
			continue;
		}
		while (isset($sorted_target_drives[$available_space])) {
			$available_space -= 0.001;
		}
		$sorted_target_drives[$available_space] = $target_drive;
	}
	if ($dir_selection_algorithm == 'most_available_space') {
		krsort($sorted_target_drives);
		krsort($last_resort_sorted_target_drives);
	}
	
	// Email notification when all dirs are over-capacity
	if (count($sorted_target_drives) == 0) {
		gh_log(WARN, "  Warning! All storage pool directories are over-capacity!");
		if (!isset($last_OOS_notification)) {
			$query = sprintf("SELECT value FROM settings WHERE name = '%s'", 'last_OOS_notification');
			$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for 'last_OOS_notification': " . db_error());
			$row = db_fetch_object($result);
			if ($row === FALSE || db_fetch_object($result) !== FALSE) {
				gh_log(WARN, "Received " . ($row === FALSE ? 'no rows' : 'more than one row') . " when querying settings for 'last_OOS_notification'; expected one.");
				$query = "INSERT INTO settings (name, value) VALUES ('last_OOS_notification', '0')";
				db_query($query) or gh_log(CRITICAL, "Can't insert new settings for last_OOS_notification: " . db_error());
				$row = (object) array('value' => 0);
			}
			db_free_result($result);
			$last_OOS_notification = $row->value;
		}
		if ($last_OOS_notification < strtotime('-1 day')) {
			global $email_to;

			gh_log(INFO, "  Sending email notification to $email_to");

			$hostname = exec('hostname');
			$body = "This is an automated email from Greyhole.

It appears all the defined storage pool directories are over-capacity.
You probably want to do something about this!

";
			foreach ($last_resort_sorted_target_drives as $free_space => $target_drive) {
				$minimum_free_space = (int) (isset($minimum_free_space_pool_directories[$target_drive]) ? $minimum_free_space_pool_directories[$target_drive] : 0);
				$body .= "$target_drive has " . number_format($free_space/1024/1024, 2) . " GB free; minimum specified in greyhole.conf: $minimum_free_space GB.\n";
			}
			mail($email_to, "Greyhole is out of space on $hostname!", $body);
			
			$last_OOS_notification = time();
			$query = sprintf("UPDATE settings SET value = '%s' WHERE name = '%s'", db_escape_string($last_OOS_notification), 'last_OOS_notification');
			db_query($query) or gh_log(CRITICAL, "Can't update settings for 'last_OOS_notification': " . db_error());
		}
	}

	if ($dir_selection_algorithm == 'random') {
		shuffle($sorted_target_drives);
		shuffle($last_resort_sorted_target_drives);
	}
	
	global $log_level;
	if ($log_level >= DEBUG) {
		if (count($sorted_target_drives) > 0) {
			$log = $log_prefix ."Drives with available space: ";
			foreach ($sorted_target_drives as $s => $d) {
				$log .= "$d (" . bytes_to_human($s*1024, FALSE) . " avail) - ";
			}
			gh_log(DEBUG, mb_substr($log, 0, mb_strlen($log)-2));
		}
		if (count($last_resort_sorted_target_drives) > 0) {
			$log = $log_prefix ."Drives with enough free space, but no available space: ";
			foreach ($last_resort_sorted_target_drives as $s => $d) {
				$log .= "$d (" . bytes_to_human($s*1024, FALSE) . " avail) - ";
			}
			gh_log(DEBUG, mb_substr($log, 0, mb_strlen($log)-2));
		}
		if (count($full_drives) > 0) {
			$log = $log_prefix ."Drives full: ";
			foreach ($full_drives as $s => $d) {
				$log .= "$d (" . bytes_to_human($s*1024, FALSE) . " free) - ";
			}
			gh_log(DEBUG, mb_substr($log, 0, mb_strlen($log)-2));
		}
	}
	
	$drives = array_merge($sorted_target_drives, $last_resort_sorted_target_drives);
	if ($include_full_drives) {
		$drives = array_merge($drives, $full_drives);
	}

	if (isset($sticky_files)) {
		global $is_sticky;
		$is_sticky = FALSE;
		foreach ($sticky_files as $share_dir => $stick_into) {
			if (gh_wild_mb_strpos("$share/$path", $share_dir) === 0) {
				$is_sticky = TRUE;

 				$more_drives_needed = FALSE;
				if (count($stick_into) > 0) {
					// Stick files into specific drives: $stick_into
					// Let's check if those drives are listed in the config file!
					foreach ($stick_into as $key => $stick_into_dir) {
						if (array_search($stick_into_dir, $storage_pool_directories) === FALSE) {
							unset($stick_into[$key]);
							$more_drives_needed = TRUE;
						}
					}
				}
				if (count($stick_into) == 0 || $more_drives_needed) {
					if (mb_strpos($share_dir, '*') !== FALSE) {
						// Contains a wildcard... In this case, we want each directory that match the wildcard to have it's own setting. Let's find this directory...
						// For example, if $share_dir == 'Videos/Movies/*/*' and "$share/$path/" == "Videos/Movies/HD/La Vita e Bella/", we want to save a 'stick_into' setting for 'Videos/Movies/HD/La Vita e Bella/'
						// Files in other subdirectories of Videos/Movies/HD/ could end up in other drives.
						$needles = explode('*', $share_dir);
						$sticky_dir = '';
						$wild_part = "$share/$path/";
						for ($i=0; $i<count($needles); $i++) {
							$needle = $needles[$i];
							if ($i == 0) {
								$sticky_dir = $needle;
								$wild_part = @str_replace_first($needle, '', $wild_part);
							} else {
								if ($needle == '') {
									$needle = '/';
								}
								$small_wild_part = mb_substr($wild_part, 0, mb_strpos($wild_part, $needle)+mb_strlen($needle));
								$sticky_dir .= $small_wild_part;
								$wild_part = str_replace_first($small_wild_part, '', $wild_part);
							}
						}
						$sticky_dir = trim($sticky_dir, '/');
					} else {
						$sticky_dir = $share_dir;
					}

					// Stick files into any drives
					$query = sprintf("SELECT value FROM settings WHERE name = 'stick_into-%s'",
						db_escape_string($sticky_dir)
					);
					$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for stick_target-$sticky_dir: " . db_error());
					$row = db_fetch_object($result);
					if ($row !== FALSE) {
						$stick_into = array_merge($stick_into, unserialize($row->value));
						// Let's check if those drives are listed in the config file!
						$update_needed = FALSE;
						foreach ($stick_into as $key => $stick_into_dir) {
							if (array_search($stick_into_dir, $storage_pool_directories) === FALSE) {
								unset($stick_into[$key]);
								$update_needed = TRUE;
							}
						}
						if ($update_needed) {
							$value = serialize($stick_into);
							$query = sprintf("UPDATE settings SET value = '%s' WHERE name = 'stick_into-%s'",
								db_escape_string($value),
								db_escape_string($sticky_dir)
							);
							db_query($query) or gh_log(CRITICAL, "Can't update settings for stick_into-$sticky_dir: " . db_error());
						}
					} else {
						$value = serialize(array_merge($stick_into, $drives));
						$query = sprintf("INSERT INTO settings (name, value) VALUES ('stick_into-%s', '%s')",
							db_escape_string($sticky_dir),
							db_escape_string($value)
						);
						db_query($query) or gh_log(CRITICAL, "Can't insert new settings for stick_into-$sticky_dir: " . db_error());
					}
				}
				
				// Make sure the drives we want to use are not yet full
				$priority_drives = array();
				foreach ($stick_into as $stick_into_dir) {
					if (array_search($stick_into_dir, $full_drives) === FALSE && array_search($stick_into_dir, $storage_pool_directories) !== FALSE) {
						unset($drives[array_search($stick_into_dir, $drives)]);
						$priority_drives[] = $stick_into_dir;
					}
				}
				$drives = array_merge($priority_drives, $drives);
				gh_log(DEBUG, $log_prefix . "Reordered drives, per sticky_files config: " . implode(' - ', $drives));
				break;
			}
		}
	}

	return $drives;
}

function gh_fsck($path, $share, $storage_path = FALSE, $find_orphans = FALSE) {
	global $storage_pool_directories, $fsck_report;
	
	gh_log(DEBUG, "Entering $path");
	$fsck_report['landing_zone']['num_dirs']++;

	$list = array();
	$handle = opendir($path);
	if ($handle === FALSE) {
		gh_log(ERROR, "  Couldn't open $path to list content. Skipping...");
		return;
	}
	while (($filename = readdir($handle)) !== FALSE) {
		if ($filename != '.' && $filename != '..') {
			$full_path = "$path/$filename";
			$file_type = @filetype($full_path);
			if ($file_type == 'dir') {
				gh_fsck($full_path, $share, $storage_path, $find_orphans);
			} else {
				$fsck_report['landing_zone']['num_files']++;
				gh_fsck_file($path, $filename, $file_type, 'landing_zone', $share, $storage_path, $find_orphans);
			}
		}
	}
	closedir($handle);
}

function gh_fsck_graveyard($root, $path, $share) {
	global $fsck_report;
	
	gh_log(DEBUG, "Entering graveyard $root$path");
	if (!is_dir("$root$path")) {
		gh_log(DEBUG, "  Not a directory... Aborting.");
		return;
	}

	$handle = opendir("$root$path");
	while (($filename = readdir($handle)) !== FALSE) {
		if ($filename != '.' && $filename != '..') {
			if (@is_dir("$root$path/$filename")) {
				$fsck_report['graveyard']['num_dirs']++;
				gh_fsck_graveyard($root, "$path/$filename", $share);
			} else {
				// Found a tombstone
				$fsck_report['graveyard']['num_files']++;
				
				$path_parts = explode('/', $path);
				array_shift($path_parts);
				$share = array_shift($path_parts);
				$landing_zone = get_share_landing_zone($share);
				$local_path = $landing_zone . '/' . implode('/', $path_parts);
				
				// If file exists in landing zone, we already fsck-ed it in gh_fsck(); let's not repeat ourselves, shall we?
				if (!file_exists("$local_path/$filename")) {
					gh_fsck_file($local_path, $filename, FALSE, 'graveyard', $share);
				}
			}
		}
	}
	closedir($handle);
}

function gh_fsck_file($path, $filename, $file_type, $source, $share, $storage_path = FALSE, $find_orphans = FALSE) {
	global $storage_pool_directories, $fsck_report, $options, $action;
	$landing_zone = get_share_landing_zone($share);
	if($storage_path === FALSE){
		$file_path = trim(mb_substr($path, mb_strlen($landing_zone)+1), '/');
	}else{
		$file_path = trim(mb_substr($path, mb_strlen("$storage_path/$share")+1), '/');
	}
	if ($file_type == 'file') {
		if($storage_path === FALSE){
			// Let's just add a 'write' task for this file; if it's a duplicate of an already pending task, it won't be processed twice, since the simplify function will remove such duplicates.
			gh_log(INFO, "$path/$filename is a file (not a symlink). Adding a new 'write' pending task for that file.");
			$query = sprintf("INSERT INTO tasks (action, share, full_path, complete) VALUES ('write', '%s', '%s', 'yes')",
				db_escape_string($share),
				db_escape_string(empty($file_path) ? $filename : clean_dir("$file_path/$filename"))
			);
			db_query($query) or gh_log(CRITICAL, "Can't insert write task: " . db_error());
			return;
		}
	} else {
		if($storage_path === FALSE){
			if ($file_type == 'link' && !file_exists(readlink("$path/$filename"))) {
				// Link points to now gone copy; let's just remove it, and treat this as if the link was not there in the first place.
				unlink("$path/$filename");
				$file_type = FALSE;
			}
			if ($file_type === FALSE) {
				if ($action != 'fsck_file') {
					// Maybe this file was removed after fsck started, and thus shouldn't be re-created here!
					// We'll queue this file fsck (to restore the symlink) for when all other file operations have been executed.
					$query = sprintf("INSERT INTO tasks (action, share, full_path, complete) VALUES ('fsck_file', '%s', '%s', 'idle')",
						db_escape_string($share),
						db_escape_string(empty($file_path) ? $filename : clean_dir("$file_path/$filename"))
					);
					db_query($query) or gh_log(CRITICAL, "Can't insert write task: " . db_error());
					return;
				}
			}
		}
	}
	
	$file_tombstones = array();
	$num_ok = 0;
	$file_copies_inodes = array();
	if(get_tombstone_data_filename($share,$file_path,$filename) === FALSE){
		if($storage_path !== FALSE && $find_orphans !== FALSE){
			gh_log(INFO, "$path/$filename is an orphaned file, will proceed to find all copies and symlink file appropriately.");
			$fsck_report['orphaned']['num_orphans']++;
		}elseif($storage_path !== FALSE && $find_orphans === FALSE){
			gh_log(INFO, "$path/$filename is an orphaned file, but we're not looking for orphans, to get greyhole to recognized this file initiate an fsck with the --find-orphaned-files option.");
			return;
		}
	}
			
	// Look for this file on all available drives
	foreach ($storage_pool_directories as $target_drive) {
		$clean_full_path = clean_dir("$target_drive/$share/$file_path/$filename");
		$inode_number = @gh_fileinode($clean_full_path);
		if ($inode_number !== FALSE) {
			if (is_dir($clean_full_path)) {
				gh_log(DEBUG, "Found a directory that should be a file! Will try to remove it, if it's empty.");
				@rmdir($clean_full_path);
				continue;
			}

			gh_log(DEBUG, "Found $clean_full_path");

			if (!is_greyhole_owned_dir($target_drive)) {
				gh_log(DEBUG, "  Directory $target_drive is not part of the Greyhole storage pool anymore. The above file will not be counted as a valid file copy, but can be used to create a new valid copy.");
				$file_tombstones[$clean_full_path] = (object) array('path' => $clean_full_path, 'is_linked' => FALSE, 'state' => 'Gone');
				if (!isset($original_file_path)) {
					$original_file_path = $clean_full_path;
				}
			} else {
				$file_tombstones[$clean_full_path] = (object) array('path' => $clean_full_path, 'is_linked' => FALSE, 'state' => 'OK');
				$file_copies_inodes[$inode_number] = $clean_full_path;
				$num_ok++;
			}
			
			// Temp files leftovers of stopped Greyhole executions
			$temp_filename = get_temp_filename($clean_full_path);
			if (file_exists($temp_filename) && gh_is_file($temp_filename)) {
				gh_log(INFO, "  Found temporary file $temp_filename ... deleting.");
				$fsck_report['temp_files'][] = $temp_filename;
				gh_recycle($temp_filename);
			}
		}
	}
	foreach (get_tombstones($share, $file_path, $filename, TRUE) as $tombstone_block){
		foreach ($tombstone_block as $tombstone) {
			$inode_number = @gh_fileinode($tombstone->path);
			if ($inode_number === FALSE) {
				$tombstone->state = 'Gone';
				$tombstone->is_linked = FALSE;

				$root_path = str_replace(clean_dir("/$share/$file_path/$filename"), '', $tombstone->path);
				if (gone_ok($root_path)) {
					// Let's not replace this copy yet...
					$file_copies_inodes[$tombstone->path] = $tombstone->path;
					$num_ok++;
					$fsck_report['gone_ok']++;
				}
			} else if (is_dir($tombstone->path)) {
				gh_log(DEBUG, "Found a directory that should be a file! Will try to remove it, if it's empty.");
				@rmdir($tombstone->path);
				$tombstone->state = 'Gone';
				$tombstone->is_linked = FALSE;
				continue;
			} else {
				$tombstone->state = 'OK';
				if (!isset($file_tombstones[$tombstone->path])) {
					$file_copies_inodes[$inode_number] = $tombstone->path;
					$num_ok++;
				}
			}
			$file_tombstones[clean_dir($tombstone->path)] = $tombstone;
		}
	}

	if (count($file_copies_inodes) > 0) {
		// If no tombstone is linked, link the 1st one
		$found_linked_tombstone = FALSE;
		foreach ($file_tombstones as $key => $tombstone) {
			if ($tombstone->is_linked) {
				if (file_exists($tombstone->path)) {
					$found_linked_tombstone = TRUE;
					$expected_file_size = gh_filesize($tombstone->path);
					$original_file_path = $tombstone->path;
					break;
				} else {
					$tombstone->is_linked = FALSE;
					$tombstone->state = 'Gone';
				}
			}
		}
		if (!$found_linked_tombstone) {
			$first_tombstone = reset($file_tombstones);
			$first_tombstone->is_linked = TRUE;
			$expected_file_size = gh_filesize($first_tombstone->path);
			$original_file_path = $first_tombstone->path;
		}
		
		if (isset($options['disk-usage-report'])) {
			global $du;
			$du_path = '/' . trim(clean_dir("/$share/$file_path"), '/');
			do {
				if (!isset($du[$du_path])) {
					$du[$du_path] = 0;
				}
				$du[$du_path] += $expected_file_size;
				$du_path = mb_substr($du_path, 0, mb_strrpos($du_path, '/'));
			} while (!empty($du_path));
		}
		
		// Check that all file copies have the same size
		foreach ($file_copies_inodes as $key => $real_full_path) {
			if (array_search($real_full_path, array_keys($file_copies_inodes)) !== FALSE) {
				// That file isn't available atm, but it's OK.
				continue;
			}
			$file_size = gh_filesize($real_full_path);
			if ($file_size != $expected_file_size) {
				// Found a file with a different size than the original...
				// There might be a good reason. Let's look for one!
				if (real_file_is_locked($real_full_path) !== FALSE || real_file_is_locked($original_file_path) !== FALSE) {
					// Write operation in progress
					continue;
				}
				// A pending write transaction maybe?
				parse_samba_spool(FALSE);
				$query = sprintf("SELECT * FROM tasks WHERE action = 'write' AND share = '%s' AND full_path = '%s'",
					db_escape_string($share),
					db_escape_string("$file_path/$filename")
				);
				$result = db_query($query) or gh_log(CRITICAL, "Can't select for pending writes" . db_error());
				if (db_fetch_object($result) !== FALSE) {
					// Pending write task
					continue;
				}
				// Found no good reason!

				if ($file_size == 0) {
					// Empty file; just delete it.
					gh_log(WARN, "  An empty file copy was found: $real_full_path is 0 bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes. This empty copy will be deleted.");
					unlink($real_full_path);
				} else {
					gh_log(WARN, "  A file copy with a different file size than the original was found: $real_full_path is " . number_format($file_size) . " bytes. Original: $original_file_path is " . number_format($expected_file_size) . " bytes.");
					gh_recycle($real_full_path);
					$fsck_report['wrong_file_size'][clean_dir($real_full_path)] = array($file_size, $expected_file_size, $original_file_path);
				}
				// Will not count that copy as a valid copy!
				unset($file_copies_inodes[$key]);
				unset($file_tombstones[clean_dir($real_full_path)]);
			}
		}
	}

	$num_copies_required = get_num_copies($share);
	
	if (count($file_copies_inodes) == $num_copies_required) {
		// It's okay if the file isn't a symlink so long as we're looking at a storage volume path and not a share path
		if (!$found_linked_tombstone || ($file_type != 'link' && $storage_path === FALSE)) {
			// Re-create symlink...
			if (!$found_linked_tombstone) {
				// ... the old one points to a drive that was replaced
				gh_log(INFO, '  Symlink target moved. Updating symlink.');
				$fsck_report['symlink_target_moved']++;
			} else {
				// ... it was missing
				gh_log(INFO, '  Symlink was missing. Creating new symlink.');
			}
			foreach ($file_tombstones as $key => $tombstone) {
				if ($tombstone->is_linked) {
					gh_log(DEBUG, "  Updating symlink at $landing_zone/$file_path/$filename to point to $tombstone->path");
					@unlink("$landing_zone/$file_path/$filename");
					list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);
					gh_mkdir("$landing_zone/$file_path", $tombstone_dir_path);
					symlink($tombstone->path, "$landing_zone/$file_path/$filename");
					break;
				}
			}
			save_tombstones($share, $file_path, $filename, array_values($file_tombstones));
		}
	} else if (count($file_copies_inodes) == 0 && !isset($original_file_path)) {
		gh_log(WARN, '  WARNING! No copies of this file are available in the Greyhole storage pool. ' . (is_link("$landing_zone/$file_path/$filename") ? 'Deleting from share.' : (gh_is_file("$landing_zone/$file_path/$filename") ? 'Did you copy that file there without using your Samba shares? (If you did, don\'t do that in the future.)' : '')));
		if (is_link("$landing_zone/$file_path/$filename")) {
			gh_recycle("$landing_zone/$file_path/$filename");
			if ($source == 'graveyard' || get_tombstone_data_filename($share, $file_path, $filename) !== FALSE) {
				$fsck_report['no_copies_found_files'][clean_dir("$share/$file_path/$filename")] = TRUE;
			}
		} else if (gh_is_file("$landing_zone/$file_path/$filename")) {
			gh_log(INFO, "$share/$file_path/$filename is a file (not a symlink). Adding a new 'write' pending task for that file.");
			$query = sprintf("INSERT INTO tasks (action, share, full_path, complete) VALUES ('write', '%s', '%s', 'yes')",
				db_escape_string($share),
				db_escape_string(empty($file_path) ? $filename : clean_dir("$file_path/$filename"))
			);
			db_query($query) or gh_log(CRITICAL, "Can't insert write task: " . db_error());
		}
		save_tombstones($share, $file_path, $filename, array_values($file_tombstones));
	} else if (count($file_copies_inodes) < $num_copies_required) {
		// Create new copies
		gh_log(INFO, "  Missing file copies. Expected $num_copies_required, got " . count($file_copies_inodes) . ". Will create more copies using $original_file_path");
		if (isset($fsck_report['missing_copies'])) {
			$fsck_report['missing_copies']++;
		}
		clearstatcache(); $filesize = gh_filesize("$original_file_path");
		$file_tombstones = create_tombstones($share, "$file_path/$filename", $num_copies_required, $filesize, $file_tombstones);

		// Re-copy the file everywhere, and re-create the symlink
		$symlink_created = FALSE;
		$num_copies_current = 1; # the source file
		global $going_dir;
		if (!empty($going_dir)) {
			// Let's not count the source file here, since it will be gone soon!
			$num_copies_current = 0;
		}
		foreach ($file_tombstones as $key => $tombstone) {
			if ($original_file_path != $tombstone->path) {
				if ($num_copies_current >= $num_copies_required) {
					$tombstone->state = 'Gone';
					$file_tombstones[$key] = $tombstone;
					continue;
				}

				list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);

				if ($tombstone->state == 'Gone') {
					foreach ($storage_pool_directories as $target_drive) {
						if (get_storage_volume_from_path($tombstone_dir_path) == $target_drive && is_greyhole_owned_dir($target_drive)) {
							$tombstone->state = 'Pending';
							$file_tombstones[$key] = $tombstone;
							break;
						}
					}
				}

				if ($tombstone->state != 'Gone') {
					$root_path = str_replace(clean_dir("/$share/$file_path"), '', $tombstone_dir_path);
					list($original_path, $tombstone_filename) = explode_full_path(get_share_landing_zone($share) . "/$file_path");
					if (!gh_mkdir($tombstone_dir_path, $original_path)) {
						$tombstone->state = 'Gone';
						$file_tombstones[$key] = $tombstone;
						continue;
					}
				}
				
				if (!is_dir($tombstone_dir_path) || $tombstone->state == 'Gone') {
					continue;
				}

				if ($tombstone->state == 'Pending') {
					gh_log(DEBUG, "  Copying file to $tombstone->path");
					$temp_path = get_temp_filename($tombstone->path);
					exec("rsync -t " . escapeshellarg($original_file_path) . " " . escapeshellarg($temp_path));
					$it_worked = file_exists($temp_path) && file_exists($original_file_path) && gh_filesize($temp_path) == gh_filesize($original_file_path);
					if ($it_worked) {
						rename($temp_path, $tombstone->path);
						gh_chperm($tombstone->path, gh_get_file_infos($original_file_path));
						$tombstone->state = 'OK';
						$num_copies_current++;
					} else {
						gh_log(WARN, "    Failed file copy. Skipping.");
						if ($tombstone->is_linked) {
							$tombstone->is_linked = FALSE;
						}
						$tombstone->state = 'Gone';
					}
					$file_tombstones[$key] = $tombstone;
				}
			}
			if ($original_file_path == $tombstone->path || $tombstone->is_linked) {
				if (!empty($going_dir) && get_storage_volume_from_path($original_file_path) == $going_dir) {
					$tombstone->is_linked = FALSE;
					$tombstone->state = 'Gone';
					$file_tombstones[$key] = $tombstone;
					continue;
				}
				if ($symlink_created /* already */) {
					$tombstone->is_linked = FALSE;
					$file_tombstones[$key] = $tombstone;
					continue;
				}
				gh_log(DEBUG, "  Updating symlink at $landing_zone/$file_path/$filename to point to $tombstone->path");
				gh_recycle("$landing_zone/$file_path/$filename");
				list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);
				gh_mkdir("$landing_zone/$file_path", $tombstone_dir_path);
				symlink($tombstone->path, "$landing_zone/$file_path/$filename");
				$symlink_created = TRUE;
			}
		}
		if (!$symlink_created) {
			foreach ($file_tombstones as $key => $tombstone) {
				if ($tombstone->state == 'OK') {
					$tombstone->is_linked = TRUE;
					$file_tombstones[$key] = $tombstone;
					gh_log(DEBUG, "  Updating symlink at $landing_zone/$file_path/$filename to point to $tombstone->path");
					gh_recycle("$landing_zone/$file_path/$filename");
					list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);
					gh_mkdir("$landing_zone/$file_path", $tombstone_dir_path);
					symlink($tombstone->path, "$landing_zone/$file_path/$filename");
					break;
				}
			}
		}
		save_tombstones($share, $file_path, $filename, array_values($file_tombstones));
	} else {
		# Let's not assume that files on missing drives are really there... Removing files here could be dangerous!
		foreach ($file_copies_inodes as $inode => $path) {
			if (mb_strpos($inode, '/') === 0) {
				unset($file_copies_inodes[$inode]);
			}
		}
		if (count($file_copies_inodes) > $num_copies_required) {
			gh_log(INFO, "  Too many file copies. Expected $num_copies_required, got " . count($file_copies_inodes) . ". Will try to remove some.");
			if (file_is_locked($share, "$file_path/$filename") !== FALSE) {
				gh_log(INFO, "  File is locked. Will not remove copies at this time. The next fsck will try to remove copies again.");
				return;
			}
			$fsck_report['too_many_copies']++;
		
			$local_target_drives = array_values(order_target_drives(0, TRUE, $share, $file_path));
			while (count($file_copies_inodes) > $num_copies_required && !empty($local_target_drives)) {
				$target_drive = array_pop($local_target_drives);
				$key = clean_dir("$target_drive/$share/$file_path/$filename");
				gh_log(DEBUG, "  Looking for copy at $key");
				if (isset($file_tombstones[$key]) || gh_file_exists($key)) {
					if (isset($file_tombstones[$key])) {
						$tombstone = $file_tombstones[$key];
					}
					if (gh_file_exists($key) || $tombstone->state == 'OK') {
						gh_log(DEBUG, "    Found file copy at $key, or tombstone is marked OK.");
						if (real_file_is_locked($key) !== FALSE) {
							gh_log(DEBUG, "    File copy is locked. Won't remove it.");
							continue;
						}
						$fsck_report['too_many_files'][] = $key;
						gh_log(DEBUG, "    Removing copy at $key");
						unset($file_copies_inodes[gh_fileinode($key)]);
						gh_recycle($key);
						if (isset($file_tombstones[$key])) {
							unset($file_tombstones[$key]);
						}
						$num_ok--;
					}
				}
			}

			// If no tombstone is linked, link the 1st one
			$found_linked_tombstone = FALSE;
			foreach ($file_tombstones as $key => $tombstone) {
				if ($tombstone->is_linked) {
					$found_linked_tombstone = TRUE;
					break;
				}
			}
			if (!$found_linked_tombstone) {
				$tombstone = reset($file_tombstones);
				gh_log(DEBUG, "  Updating symlink at $landing_zone/$file_path/$filename to point to $tombstone->path");
				gh_recycle("$landing_zone/$file_path/$filename");
				list($tombstone_dir_path, $tombstone_filename) = explode_full_path($tombstone->path);
				gh_mkdir("$landing_zone/$file_path", $tombstone_dir_path);
				symlink($tombstone->path, "$landing_zone/$file_path/$filename");
				reset($file_tombstones)->is_linked = TRUE;
			}

			save_tombstones($share, $file_path, $filename, array_values($file_tombstones));
		}
	}
}

function initialize_fsck_report($what) {
	global $fsck_report;
	$fsck_report = array();
	$fsck_report['start'] = time();
	$fsck_report['what'] = $what;
	$fsck_report['graveyard'] = array();
	$fsck_report['graveyard']['num_dirs'] = 0;
	$fsck_report['graveyard']['num_files'] = 0;
	$fsck_report['orphaned']['num_orphans'] = 0;
	$fsck_report['landing_zone'] = array();
	$fsck_report['landing_zone']['num_dirs'] = 0;
	$fsck_report['landing_zone']['num_files'] = 0;
	$fsck_report['no_copies_found_files'] = array();
	$fsck_report['symlink_target_moved'] = 0;
	$fsck_report['too_many_copies'] = 0;
	$fsck_report['too_many_files'] = array();
	$fsck_report['missing_copies'] = 0;
	$fsck_report['wrong_file_size'] = array();
	$fsck_report['temp_files'] = array();
	$fsck_report['gone_ok'] = 0;
}

function get_fsck_report() {
	global $fsck_report, $storage_pool_directories, $gone_ok_drives;
	
	$fsck_report['end'] = time();
	
	$displayable_duration = duration_to_human($fsck_report['end'] - $fsck_report['start']);
	
	$report = "fsck report
-----------
Scanned directory: " . $fsck_report['what'] . "

Started:  " . date('Y-m-d H:i:s', $fsck_report['start']) . "
Ended:    " . date('Y-m-d H:i:s', $fsck_report['end']) . "
Duration: $displayable_duration

Graveyard:
  Found " . number_format($fsck_report['graveyard']['num_dirs']) . " directories
  Found " . number_format($fsck_report['graveyard']['num_files']) . " files

Landing Zone (shares):
  Found " . number_format($fsck_report['landing_zone']['num_dirs']) . " directories
  Found " . number_format($fsck_report['landing_zone']['num_files']) . " files
  Found " . number_format($fsck_report['orphaned']['num_orphans']) . " orphans

Attic size:\n";

	foreach ($storage_pool_directories as $dir) {
		$attic_path = clean_dir("$dir/.gh_attic");
		if (is_dir($attic_path)) {
			$report .= "  $attic_path = " . trim(exec("du -sh " . escapeshellarg($attic_path) . " | awk '{print $1}'"))."\n";
		} else {
			$report .= "  $attic_path = empty\n";
		}
	}

	// Errors
	if (empty($fsck_report['no_copies_found_files']) && count($fsck_report['wrong_file_size']) == 0) {
		$report .= "\nNo problems found.\n\n";
	} else {
		$report .= "\nProblems:\n";

		if (!empty($fsck_report['no_copies_found_files'])) {
			ksort($fsck_report['no_copies_found_files']);
			$report .= "  Found " . count($fsck_report['no_copies_found_files']) . " files in the graveyard for which no file copies were found.
    Those files were removed from the Landing Zone. (i.e. those files are now gone!) They will re-appear in your shares if a copy re-appear and fsck is run.
    If you don't want to see those files listed here each time fsck runs, delete the corresponding files from the graveyard.
  Files with no copies:\n";
			$report .= "    " . implode("\n    ", array_keys($fsck_report['no_copies_found_files'])) . "\n\n";
		}

		if (count($fsck_report['wrong_file_size']) > 0) {
			$report .= "  Found " . count($fsck_report['wrong_file_size']) . " file copies with the wrong file size. Those files don't have the same file size as the original files available on your shares. The invalid copies have been moved into the attic.\n";
			foreach ($fsck_report['wrong_file_size'] as $real_file_path => $info_array) {
				$report .= "    $real_file_path is " . number_format($info_array[0]) . " bytes; should be " . number_format($info_array[1]) . " bytes.\n";
			}
			$report .= "\n\n";
		}
	}

	// Warnings
	if ($fsck_report['too_many_copies'] == 0 && $fsck_report['symlink_target_moved'] == 0 && count($fsck_report['temp_files']) == 0 && $fsck_report['gone_ok'] == 0) {
		// Nothing to say...
	} else {
		$report .= "Notices:\n";

		if ($fsck_report['too_many_copies'] > 0) {
			$fsck_report['too_many_files'] = array_unique($fsck_report['too_many_files']);
		
			$report .= "  Found " . $fsck_report['too_many_copies'] . " files for which there was too many file copies. Deleted (or moved in attic) files:\n";
			$report .= "    " . implode("\n    ", $fsck_report['too_many_files']) . "\n\n";
		}

		if ($fsck_report['symlink_target_moved'] > 0) {
			$report .= "  Found " . $fsck_report['symlink_target_moved'] . " files in the Landing Zone that were pointing to a now gone copy.
    Those symlinks were updated to point to the new location of those file copies.\n\n";
		}

		if (count($fsck_report['temp_files']) > 0) {
			$report .= "  Found " . count($fsck_report['temp_files']) . " temporary files, which are leftovers of interrupted Greyhole executions. The following temporary files were deleted (or moved into the attic):\n";
			$report .= "    " . implode("\n    ", $fsck_report['temp_files']) . "\n\n";
		}
	
		if ($fsck_report['gone_ok'] > 0) {
			$report .= "  Found " . $fsck_report['gone_ok'] . " missing files that are in a storage pool directory marked Temporarily-Gone.
  If this directory is gone for good, you should execute the following command, and remove the directory from your configuration file:
    greyhole --gone=path
  where path is one of:\n";
	    	$report .= "    " . implode("\n    ", array_keys($gone_ok_drives)) . "\n\n";
		}
	}

	return $report;
}

function gh_recycle($real_path) {
	$is_symlink = FALSE;
	clearstatcache();
	if (is_link($real_path)) {
		$is_symlink = TRUE;
	} else if (!file_exists($real_path)) {
		return TRUE;
	}

	$should_move_to_attic = FALSE;
	if (!$is_symlink) {
		global $storage_pool_directories, $delete_moves_to_attic, $shares_options;
		
		$share_options = get_share_options_from_full_path($real_path);
		if ($share_options !== FALSE) {
			$full_path = trim($share_options['name'] . "/" . str_replace($share_options['landing_zone'], '', $real_path), '/');
			$share = $share_options['name'];
		} else {
			foreach ($storage_pool_directories as $dir) {
				if (get_storage_volume_from_path($real_path) == $dir) {
					$attic_path = "$dir/.gh_attic";
					$full_path = trim(str_replace($dir, '', $real_path), '/');
					break;
				}
			}

			$share = mb_substr($full_path, 0, mb_strpos($full_path, '/'));

			if (isset($shares_options[$share]['delete_moves_to_attic'])) {
				$should_move_to_attic = $shares_options[$share]['delete_moves_to_attic'];
			} else {
				$should_move_to_attic = $delete_moves_to_attic;
			}
		}
	}
	
	if ($should_move_to_attic) {
		// Move to attic
		if (!isset($attic_path)) {
			gh_log(WARN, "  Warning! Can't find attic for $real_path. Won't delete this file!");
			return FALSE;
		}
		
		$target_path = clean_dir("$attic_path/$full_path");

		list($path, $filename) = explode_full_path($target_path);
		
		list($original_path, $filename) = explode_full_path(get_share_landing_zone($share) . "/$full_path");
		
		if (@gh_is_file($path)) {
			unlink($path);
		}
		
		$dir_infos = (object) array(
			'fileowner' => 0,
			'filegroup' => 0,
			'fileperms' => (int) base_convert("0777", 8, 10)
		);
		gh_mkdir($path, $dir_infos);

		if (@is_dir($target_path)) {
			exec("rm -rf " . escapeshellarg($target_path));
		}
		if (@rename($real_path, $target_path)) {
			gh_log(DEBUG, "  Moved copy from $real_path to attic: $target_path");
			
			// Create a symlink in the Greyhole Attic share, to allow the user to remove this file using that share
			create_attic_share_symlink($target_path, $attic_path);
			return TRUE;
		}
	} else {
		if (@unlink($real_path)) {
			if (!$is_symlink) {
				gh_log(DEBUG, "  Deleted copy at $real_path");
			}
			return TRUE;
		}
	}
	return FALSE;
}

function repair_tables() {
	global $db_use_mysql;
	if (@$db_use_mysql) {
		gh_log(INFO, "Optimizing MySQL tables...");
		db_query("REPAIR TABLE tasks") or gh_log(CRITICAL, "Can't repair tasks table: " . db_error());
		db_query("REPAIR TABLE settings") or gh_log(CRITICAL, "Can't repair settings table: " . db_error());
		// Let's repair tasks_completed only if it's broken!
		$result = db_query("SELECT * FROM tasks_completed LIMIT 1");
		if ($result === FALSE) {
			gh_log(INFO, "Repairing MySQL tables...");
			db_query("REPAIR TABLE tasks_completed") or gh_log(CRITICAL, "Can't repair tasks_completed table: " . db_error());
		}
	}
}

function check_storage_pool_dirs() {
	global $storage_pool_directories, $email_to, $gone_ok_drives;
	$needs_fsck = FALSE;
	$returned_drives = array();
	$missing_drives = array();
	$i = 0; $j = 0;
	foreach ($storage_pool_directories as $target_drive) {
		if (!is_greyhole_owned_dir($target_drive) && !gone_fscked($target_drive, $i++ == 0) && !file_exists("$target_drive/.greyhole_used_this")) {
			if($needs_fsck !== 2){	
				$needs_fsck = 1;
			}
			mark_gone_drive_fscked($target_drive);
			$missing_drives[] = $target_drive;
			gh_log(WARN, "Warning! It seems $target_drive is missing it's \".greyhole_uses_this\" file. This either means this drive is currently unmounted, or you forgot to create this file.");
			gh_log(DEBUG, "Email sent for gone dir: $target_drive");
			$gone_ok_drives[$target_drive] = TRUE; // The upcoming fsck should not recreate missing copies just yet
		} else if ((gone_ok($target_drive, $j++ == 0) || gone_fscked($target_drive, $i++ == 0)) && is_greyhole_owned_dir($target_drive)) {
			// $target_drive is now back
			$needs_fsck = 2;
			$returned_drives[] = $target_drive;
						gh_log(DEBUG, "Email sent for revived dir: $target_drive");

			mark_gone_ok($target_drive, 'remove');
			mark_gone_drive_fscked($target_drive, 'remove');
			$i = 0; $j = 0;
		}
	}
	if(count($returned_drives) > 0){
		$body = "This is an automated email from Greyhole.

It appears one or more of your storage pool directories came back:\n";
		foreach($returned_drives as $target_drive){
  			$body = $body . "$target_drive was missing; it's now available again.\n";
		}
		$body = $body . "\nA fsck will now start, to fix the symlinks found in your shares, when possible.
You'll receive a report email once that fsck run completes.
";
		$drive_string = join(",",$returned_drives);
		$subject = "Storage pool directories now online on " . exec ('hostname') . ": ";
		$subject = $subject . $drive_string;
		if(strlen($subject) > 255){
			$subject = substr($subject,0,255);
		}
		mail($email_to, $subject, $body);
	}
	if(count($missing_drives) > 0){
		$body = "This is an automated email from Greyhole.

It appears one or more of your storage pool directories are missing their \".greyhole_uses_this\" file:\n";
		foreach($missing_drives as $target_drive){
  			$body = $body . "$target_drive/.greyhole_uses_this: File not found\n";
		}
		$target_drive = $missing_drives[0];
		$body = $body . "\nThis either means these mount(s) are currently unmounted, or you forgot to create this file.

Here are your options:

- If you forgot to create this file, you should create it ASAP, as per the INSTALL instructions. Until you do, this directory will not be part of your storage pool.

- If the drive(s) are gone, you should either re-mount them manually (if possible), or remove them from your storage pool. To do so, use the following command:
  greyhole --gone=".escapeshellarg($target_drive)."
  Note that the above command is REQUIRED for Greyhole to re-create missing file copies before the next fsck runs. Until either happens, missing file copies WILL NOT be re-created on other drives.

- If you know these drive(s) will come back soon, and do NOT want Greyhole to re-create missing file copies for this directory until it reappears, you should execute this command:
  greyhole --wait-for=".escapeshellarg($target_drive)."

A fsck will now start, to fix the symlinks found in your shares, when possible.
You'll receive a report email once that fsck run completes.
";                      
			$subject = "Missing storage pool directories on " . exec ('hostname') . ": ";
			$drive_string = join(",",$missing_drives);
			$subject = $subject . $drive_string;
			if(strlen($subject) > 255){
				$subject = substr($subject,0,255);
			}
                        mail($email_to, $subject, $body);
	}
	if ($needs_fsck !== FALSE) {
		set_graveyard_backup();
		get_graveyards(FALSE); // FALSE => Resets the graveyards cache
		global $shares_options;
		
		
		initialize_fsck_report('All shares');
		clearstatcache();
		if($needs_fsck === 2){
			foreach ($returned_drives as $drive){
				$graveyards = get_graveyards_from_storage_volume($drive);
				gh_log(INFO, "Starting fsck for graveyard on $drive which came back online.");
				foreach($graveyards as $graveyard){
					foreach($shares_options as $share_name => $share_options){
						gh_fsck_graveyard($graveyard,"/$share_name", $share_name);
					}
				}
				gh_log(INFO, "fsck for returning drive $drive's graveyard completed.");
			}
			gh_log(INFO, "Starting fsck for all shares - caused by missing drive that came back online.");
		}else{
			gh_log(INFO, "Starting fsck for all shares - caused by missing drive. Will just recreate symlinks to existing copies when possible; won't create new copies just yet.");
		}
		foreach ($shares_options as $share_name => $share_options) {
			gh_fsck($share_options['landing_zone'], $share_name);
		}
		gh_log(INFO, "fsck for all shares completed.");

		$fsck_report = get_fsck_report();
		gh_log(DEBUG, "Sending fsck report to $email_to");
		mail($email_to, 'fsck of Greyhole shares on ' . exec('hostname'), $fsck_report);

		// Refresh $gone_ok_drives to it's real value (from the DB)
		get_gone_ok_dirs();
	}
}

// Is it OK for a drive to be gone?
function gone_ok($target_drive, $refresh=FALSE) {
	global $gone_ok_drives;
	if ($refresh || !isset($gone_ok_drives)) {
		$gone_ok_drives = get_gone_ok_dirs();
	}
	if (isset($gone_ok_drives[$target_drive])) {
		return TRUE;
	}
	return FALSE;
}

function get_gone_ok_dirs() {
	global $gone_ok_drives;
	$query = "SELECT value FROM settings WHERE name = 'Gone-OK-Drives'";
	$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for Gone-OK-Drives: " . db_error());
	$row = db_fetch_object($result);
	if ($row !== FALSE) {
		$gone_ok_drives = unserialize($row->value);
	} else {
		$query = "INSERT INTO settings (name, value) VALUES ('Gone-OK-Drives', 'a:0:{}')";
		db_query($query) or gh_log(CRITICAL, "Can't insert new settings for Gone-OK-Drives: " . db_error());
		$gone_ok_drives = array();
	}
	return $gone_ok_drives;
}

function mark_gone_ok($target_drive, $action='add') {
	global $storage_pool_directories;
	if (!array_search($target_drive, $storage_pool_directories)) {
		return FALSE;
	}

	global $gone_ok_drives;
	$gone_ok_drives = get_gone_ok_dirs();
	if ($action == 'add') {
		$gone_ok_drives[$target_drive] = TRUE;
	} else {
		unset($gone_ok_drives[$target_drive]);
	}

	$query = sprintf("UPDATE settings SET value = '%s' WHERE name = 'Gone-OK-Drives'",
		db_escape_string(serialize($gone_ok_drives))
	);
	db_query($query) or gh_log(CRITICAL, "Can't save settings for Gone-OK-Drives: " . db_error());
	return TRUE;
}

function gone_fscked($target_drive, $refresh=FALSE) {
	global $fscked_gone_drives;
	if ($refresh || !isset($fscked_gone_drives)) {
		$fscked_gone_drives = get_fsck_gone_drives();
	}
	if (isset($fscked_gone_drives[$target_drive])) {
		return TRUE;
	}
	return FALSE;
}

function get_fsck_gone_drives() {
	global $fscked_gone_drives;
	$query = "SELECT value FROM settings WHERE name = 'Gone-FSCKed-Drives'";
	$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for Gone-FSCKed-Drives: " . db_error());
	$row = db_fetch_object($result);
	if ($row !== FALSE) {
		$fscked_gone_drives = unserialize($row->value);
	} else {
		$query = "INSERT INTO settings (name, value) VALUES ('Gone-FSCKed-Drives', 'a:0:{}')";
		db_query($query) or gh_log(CRITICAL, "Can't insert new settings for Gone-FSCKed-Drives: " . db_error());
		$fscked_gone_drives = array();
	}
	return $fscked_gone_drives;
}

function mark_gone_drive_fscked($target_drive, $action='add') {
	global $fscked_gone_drives;
	$fscked_gone_drives = get_fsck_gone_drives();
	if ($action == 'add') {
		$fscked_gone_drives[$target_drive] = TRUE;
	} else {
		unset($fscked_gone_drives[$target_drive]);
	}

	$query = sprintf("UPDATE settings SET value = '%s' WHERE name = 'Gone-FSCKed-Drives'",
		db_escape_string(serialize($fscked_gone_drives))
	);
	db_query($query) or gh_log(CRITICAL, "Can't save settings for Gone-FSCKed-Drives: " . db_error());
}

function get_graveyards_from_storage_volume($storage_volume){
	$volume_graveyards = array();
	foreach (get_graveyards() as $graveyard) {
		if (get_storage_volume_from_path($graveyard) == $storage_volume) {
			$volume_graveyards[] = $graveyard;
		}
	}
	return $volume_graveyards;
}

function get_graveyards($use_cache=TRUE) {
	global $storage_pool_directories, $graveyard_backup_directories, $global_graveyards;
	if (!isset($global_graveyards) || !$use_cache) {
		$graveyards = array();
		foreach ($storage_pool_directories as $target_drive) {
			if (is_greyhole_owned_dir($target_drive)) {
				$graveyards[] = "$target_drive/.gh_graveyard";
			}
		}
		foreach ($graveyard_backup_directories as $graveyard_backup_directory) {
			if (is_greyhole_owned_dir(str_replace('/.gh_graveyard_backup', '', $graveyard_backup_directory))) {
				$graveyards[] = $graveyard_backup_directory;
			}
		}
		$global_graveyards = $graveyards;
	}
	return $global_graveyards;
}

function get_share_options_from_full_path($full_path) {
	global $shares_options;
	$landing_zone = '';
	$share = FALSE;
	foreach ($shares_options as $share_name => $share_options) {
		if (mb_strpos($full_path, $share_options['landing_zone']) === 0 && mb_strlen($share_options['landing_zone']) > mb_strlen($landing_zone)) {
			$landing_zone = $share_options['landing_zone'];
			$share = $share_options;
			break;
		}
	}
	return $share;


}

function get_storage_volume_from_path($full_path){
	global $storage_pool_directories;
	$storage_volume = FALSE;
	$longest_path_found = 0;
	foreach ($storage_pool_directories as $target_drive) {
		if (mb_strpos($full_path, $target_drive) === 0 && mb_strlen($target_drive) > $longest_path_found) {
			$storage_volume = $target_drive;
			$longest_path_found = mb_strlen($target_drive);
			break;
		} 
	}
	return $storage_volume;
}

function get_share_options_from_storage_volume($full_path,$storage_volume) {
	global $shares_options;
	$landing_zone = '';
	$share = FALSE;
	foreach ($shares_options as $share_name => $share_options) {
		$graveyard = get_graveyard_from_path($full_path);
		if($graveyard !== FALSE){
			if (mb_strpos($full_path, "$graveyard/$share_name") === 0 && mb_strlen($share_options['landing_zone']) > mb_strlen($landing_zone)) {
				$land_zone = $share_options['landing_zone'];
				$share = $share_options;
				break;
			}
		}else{
			if (mb_strpos($full_path, "$storage_volume/$share_name") === 0 && mb_strlen($share_options['landing_zone']) > mb_strlen($landing_zone)) {
				$landing_zone = $share_options['landing_zone'];
				$share = $share_options;
				break;
			}
		}
	}
	return $share;
}

function get_graveyard_from_path($path){
	$graveyard_path = FALSE;
	foreach(get_graveyards() as $graveyard){
		if(mb_strpos($path, $graveyard) === 0){
			$graveyard_path = $graveyard;
			break;
		}	
	}
	return $graveyard_path;
}

function gh_get_file_infos($real_path) {
	if ($real_path == null || !file_exists($real_path)) {
		return (object) array(
			'fileowner' => 0,
			'filegroup' => 0,
			'fileperms' => (int) base_convert("0777", 8, 10)
		);
	}
	if (is_link($real_path)) {
		$real_path = readlink($real_path);
	}
	return (object) array(
		'fileowner' => (int) gh_fileowner($real_path),
		'filegroup' => (int) gh_filegroup($real_path),
		'fileperms' => (int) base_convert(gh_fileperms($real_path), 8, 10)
	);
}

function gh_get_dir_perms($directory) {
	return (int) base_convert(gh_fileperms($directory), 8, 10);
}

function gh_balance() {
	global $storage_pool_directories, $shares_options, $is_sticky;

	// Start with shares that have sticky files, so that subsequent shares will be used to try to balance what moving files into stick_into drives could debalance...
	// Then start with the shares for which we keep the most # copies;
	// That way, if the new drive fails soon, it won't take with it files for which we only have one copy!
	$sorted_shares_options = $shares_options;
	uasort($sorted_shares_options, 'compare_share_balance');
	foreach ($sorted_shares_options as $share_name => $share_options) {
		if ($share_options['num_copies'] == count($storage_pool_directories)) {
			// Files are everywhere; won't be able to use that share to balance available space!
			continue;
		}
		
		if (@$skip_stickies && is_share_sticky($share_name)) {
			gh_log(DEBUG, "Skipping sticky share $share_name.");
			continue;
		}

 		gh_log(DEBUG, "Balancing share: $share_name");

		// Move files from the drive with the less available space to the drive with the most available space.
		$sorted_pool_drives = sort_storage_dirs_available_space();
		$pool_drives_avail_space = array();
		foreach ($sorted_pool_drives as $available_space => $drive) {
			$pool_drives_avail_space[$drive] = $available_space;
		}
		$num_total_drives = count($sorted_pool_drives);

		foreach ($sorted_pool_drives as $source_drive) {
			$target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
			gh_log(DEBUG, "Balancing storage pool directory: $source_drive (". bytes_to_human($pool_drives_avail_space[$source_drive]*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");

			// Files candidate to get moved
			$files = array();
			if (is_dir("$source_drive/$share_name")) {
				exec("find ". escapeshellarg("$source_drive/$share_name") ." -type f -size +10M", $files);
			}
			gh_log(DEBUG, "Found ". count($files) ." files that can be moved.");
			
			// Repeat until all drives' available space is balanced.
			$file_everywhere_counter = 0;
			foreach ($files as $file) {
				// Let's not try to move locked files!
				if (real_file_is_locked($file) !== FALSE) {
					gh_log(DEBUG, "  File $file is locked by another process. Skipping.");
					continue;
				}

				$filesize = gh_filesize($file)/1024; // KB

				$full_path = mb_substr($file, mb_strlen("$source_drive/$share_name/"));
				list($path, $filename) = explode_full_path($full_path);
				gh_log(DEBUG, "  Working on file: $share_name/$full_path (". bytes_to_human($filesize*1024, FALSE) .")");

				$target_drives = order_target_drives($filesize, FALSE, $share_name, $path, '  ');

				unset($target_drive);
				if ($is_sticky) {
					if (count($target_drives) == $num_total_drives - 1 && array_search($source_drive, $target_drives) === FALSE) {
						// Only drive full is the source drive. Let's move files away from there!
					} else if (count($target_drives) < $num_total_drives) {
						$skip_stickies = TRUE;
						gh_log(DEBUG, "  Some drives are full. Skipping sticky shares until all drives have some free space.");
						break;
					}
					
					$sticky_drives = array_slice($target_drives, 0, get_num_copies($share_name));
					if (array_search($source_drive, $sticky_drives)) {
						// Source drive is a stick_into drive; let's not move that file!
						gh_log(DEBUG, "  Source is sticky. Skipping.");
						continue;
					}
					$already_stuck_copies = 0;
					foreach ($sticky_drives as $drive) {
						if (file_exists("$drive/$share_name/$full_path")) {
							$already_stuck_copies++;
						} else {
							$target_drive = $drive;
						}
					}
				} else {
					while (count($target_drives) > 0) {
						$drive = array_shift($target_drives);
						if (!file_exists("$drive/$share_name/$full_path")) {
							$target_drive = $drive;
							break;
						}
					}
				}
				
				if (!isset($target_drive)) {
					// Can't find a drive that doesn't have this file; skipping.
					if ($is_sticky) {
						gh_log(DEBUG, "  Sticky file is already where it should be. Skipping.");
					}
					continue;
				}

				gh_log(DEBUG, "  Target drive: $target_drive (". bytes_to_human($pool_drives_avail_space[$target_drive]*1024, FALSE) ." available)");

				if ($is_sticky) {
					gh_log(DEBUG, "  Moving sticky file, even if that means it won't help balancing available space.");
				} else if ($pool_drives_avail_space[$source_drive]+$filesize+1*1024*1024 /*1GB*/ > $pool_drives_avail_space[$target_drive]-$filesize
						|| $pool_drives_avail_space[$source_drive]+$filesize > $target_avail_space
						|| $pool_drives_avail_space[$target_drive] < $target_avail_space) {
					gh_log(DEBUG, "  Moving this file wouldn't help balancing available space. Skipping.");
					continue;
				}

				// Make sure the parent directory exists, before we try moving something there...
				$original_path = clean_dir("$source_drive/$share_name/$path");
				list($target_path, $filename) = explode_full_path("$target_drive/$share_name/$full_path");
				gh_mkdir($target_path, $original_path);

				// Move the file
				$temp_path = get_temp_filename("$target_drive/$share_name/$full_path");
				$file_infos = gh_get_file_infos($file);
				gh_log(DEBUG, "  Moving file copy...");
				$it_worked = rename($file, $temp_path);
				if ($it_worked) {
					rename($temp_path, "$target_drive/$share_name/$full_path");
					gh_chperm("$target_drive/$share_name/$full_path", $file_infos);

					$pool_drives_avail_space[$target_drive] -= $filesize;
					$pool_drives_avail_space[$source_drive] += $filesize;
				} else {
					gh_log(WARN, "    Failed file copy. Skipping.");
					gh_recycle($temp_path);
					continue;
				}

				// Update tombstones
				foreach (get_tombstones($share_name, $path, $filename, FALSE, TRUE, FALSE) as $existing_tombstones){
					foreach ($existing_tombstones as $key => $tombstone) {
						if ($tombstone->path == $file) {
							$tombstone->path = "$target_drive/$share_name/$full_path";
							unset($existing_tombstones[$key]);
							$tombstone->state = 'OK';
							if ($tombstone->is_linked) {
								// Re-create correct symlink
								$landing_zone = $share_options['landing_zone'];
								if (is_link("$landing_zone/$full_path")) {
									gh_log(DEBUG, "  Updating symlink at $landing_zone/$full_path to point to $tombstone->path");
									if (gh_recycle("$landing_zone/$full_path")) {
										@symlink($tombstone->path, "$landing_zone/$full_path");
										// Creating this symlink can fail if the parent dir was removed
									}
								}
							}
							$existing_tombstones[$tombstone->path] = $tombstone;
							save_tombstones($share_name, $path, $filename, $existing_tombstones);
							break;
						}
					}
				}	
				$target_avail_space = array_sum($pool_drives_avail_space) / count($pool_drives_avail_space);
				gh_log(DEBUG, "Balancing storage pool directory: $source_drive (". bytes_to_human($pool_drives_avail_space[$source_drive]*1024, FALSE) ." available, target: ". bytes_to_human($target_avail_space*1024, FALSE) .")");
			}
			gh_log(DEBUG, "Done balancing storage pool directory: $source_drive (". bytes_to_human($pool_drives_avail_space[$source_drive]*1024, FALSE) ." available)");
			
			if (@$skip_stickies) {
				break;
			}
		}
		gh_log(DEBUG, "Done balancing share: $share_name");
	}
	
	if (@$skip_stickies) {
		// We skipped some stickies... Let's re-balance to move those, and continue balancing.
		$arr = debug_backtrace();
		if (count($arr) < 93) {
			gh_log(DEBUG, "Some shares with sticky files were skipped. Balancing will now re-start to continue moving those sticky files as needed, and further balance. Recursion level = " . count($arr));
			gh_balance();
		} else {
			gh_log(WARN, "Maximum number of consecutive balance reached. You'll need to re-execute --balance if you want to balance further.");
		}
	}
}

function is_share_sticky($share_name) {
	global $sticky_files;
	if (isset($sticky_files)) {
		foreach ($sticky_files as $share_dir => $stick_into) {
			if (mb_strpos($share_dir, $share_name) === 0) {
				return TRUE;
			}
		}
	}
	return FALSE;
}

function compare_share_balance($a, $b) {
	if (is_share_sticky($a['name']) && !is_share_sticky($b['name'])) {
		return -1;
	}
	if (!is_share_sticky($a['name']) && is_share_sticky($b['name'])) {
		return 1;
	}
	if ($a['num_copies'] != $b['num_copies']) {
		return $a['num_copies'] > $b['num_copies'] ? -1 : 1;
	}
	return strcasecmp($a['name'], $b['name']);
}

function sort_storage_dirs_available_space() {
	global $storage_pool_directories, $minimum_free_space_pool_directories;
	$dfs = get_free_space_in_storage_pool_dirs();
	foreach ($storage_pool_directories as $target_drive) {
		if (!isset($dfs[$target_drive])) {
			continue;
		}
		$free_space = $dfs[$target_drive];
		$minimum_free_space = (float) (isset($minimum_free_space_pool_directories[$target_drive]) ? $minimum_free_space_pool_directories[$target_drive]*1024*1024 : 0.0);
		$available_space = (float) $free_space - $minimum_free_space;
		while (isset($sorted_target_drives[$available_space])) {
			// In case some drives have the exact same amount of available_space, make sure we return all drives!
			$available_space++;
		}
		$sorted_target_drives[$available_space] = $target_drive;
	}
	ksort($sorted_target_drives);
	return $sorted_target_drives;
}

function postpone_task($task_id, $complete='yes') {
	global $sleep_before_task;
	$query = sprintf("INSERT INTO tasks (action, share, full_path, additional_info, complete) SELECT action, share, full_path, additional_info, '%s' FROM tasks WHERE id = %d",
		db_escape_string($complete),
		$task_id
	);
	db_query($query) or gh_log(CRITICAL, "Error inserting postponed task: " . db_error());
	$sleep_before_task[] = db_insert_id();
}

// If running on Amahi, loop until the config works.
// User might configure Greyhole later, and they don't want to show Greyhole 'offline' until then. Users are easy to confused! ;)
function process_config() {
	global $action;
	while (!parse_config()) {
		// Sleep for a while, if running on Amahi.
		if (file_exists('/usr/bin/hdactl') && $action == 'daemon') {
			sleep(600); // 10 minutes
		} else {
			// Otherwise, die.
			gh_log(CRITICAL, "Config file parsing failed. Exiting.");
		}
	}
	// Config is OK; go on!
}

function gh_wild_mb_strpos($haystack, $needle) { 
	$is_wild = (mb_strpos($needle, "*") !== FALSE);
	if (!$is_wild) {
		return mb_strpos($haystack, $needle);
	}
	if (str_replace('*', '', $needle) == $haystack) {
		return FALSE;
	}
	$needles = explode("*", $needle);
	if ($needle[0] == '*') {
		$first_index = 0;
	}
	foreach ($needles as $needle_part) {
		if ($needle_part == '') {
			continue;
		}
		$needle_index = mb_strpos($haystack, $needle_part);
		if (!isset($first_index)) {
			$first_index = $needle_index;
		}
		if ($needle_index === FALSE) {
			return FALSE;
		} else {
			$found = TRUE;
			$haystack = mb_substr($haystack, $needle_index + mb_strlen($needle_part));
		}
	}
	if ($found) {
		return $first_index;
	}
	return FALSE;
}

function str_replace_first($search, $replace, $subject) {
	$firstChar = mb_strpos($subject, $search);
	if ($firstChar !== FALSE) {
		$beforeStr = mb_substr($subject, 0, $firstChar);
		$afterStr = mb_substr($subject, $firstChar + mb_strlen($search));
		return $beforeStr . $replace . $afterStr;
	} else {
		return $subject;
	}
}

function set_graveyard_backup() {
	global $graveyard_backup_directories, $storage_pool_directories;

	$num_graveyard_backups_needed = 2;
	if (count($storage_pool_directories) < 2) {
		$graveyard_backup_directories = array();
		return;
	}

	gh_log(DEBUG, "Loading graveyard backup directories...");
	if (empty($graveyard_backup_directories)) {
		// In the DB ?
		$query = sprintf("SELECT value FROM settings WHERE name = '%s'", 'graveyard_backup_directory');
		$result = db_query($query) or gh_log(CRITICAL, "Can't query settings for 'graveyard_backup_directory': " . db_error());
		$row = db_fetch_object($result);
		db_free_result($result);
		if ($row !== FALSE) {
			$graveyard_backup_directories = unserialize($row->value);
			gh_log(DEBUG, "  Found " . count($graveyard_backup_directories) . " directories in the settings table.");
		}
	}

	// Verify the directories, if any
	if (!empty($graveyard_backup_directories)) {
		foreach ($graveyard_backup_directories as $key => $graveyard_backup_directory) {
			if (!is_greyhole_owned_dir(str_replace('/.gh_graveyard_backup', '', $graveyard_backup_directory))) {
				// Directory is now invalid; stop using it.
				gh_log(DEBUG, "Removing $graveyard_backup_directory from available 'graveyard_backup_directories' - directory isn't a greyhole storage pool directory (anymore?)");
				unset($graveyard_backup_directories[$key]);
			} else if (!is_dir($graveyard_backup_directory)) {
				// Directory is invalid, but needs to be created (was rm'ed?)
				mkdir($graveyard_backup_directory);
			}
		}
	}
	
	if (empty($graveyard_backup_directories) || count($graveyard_backup_directories) < $num_graveyard_backups_needed) {
		gh_log(DEBUG, "  Missing some directories. Need $num_graveyard_backups_needed, currently have " . count($graveyard_backup_directories) . ". Will select more...");
		$graveyard_backup_directories_hash = array();
		if (count($graveyard_backup_directories) > 0) {
			$graveyard_backup_directories_hash[array_shift($graveyard_backup_directories)] = TRUE;
		}

		while (count($graveyard_backup_directories_hash) < $num_graveyard_backups_needed) {
			// Let's pick new one
			$graveyard_backup_directory = clean_dir($storage_pool_directories[array_rand($storage_pool_directories)] . '/.gh_graveyard_backup');
			$graveyard_backup_directories_hash[$graveyard_backup_directory] = TRUE;
			if (!is_dir($graveyard_backup_directory)) {
				mkdir($graveyard_backup_directory);
			}
			gh_log(DEBUG, "    Randomly picked $graveyard_backup_directory");
		}
		$graveyard_backup_directories = array_keys($graveyard_backup_directories_hash);

		// Got 2 directories now; save them in the DB
		$query = "DELETE FROM settings WHERE name = 'graveyard_backup_directory'";
		db_query($query) or gh_log(CRITICAL, "Can't delete settings for graveyard_backup_directory: " . db_error());
		$query = sprintf("INSERT INTO settings (name, value) VALUES ('graveyard_backup_directory', '%s')",
			db_escape_string(serialize($graveyard_backup_directories))
		);
		db_query($query) or gh_log(CRITICAL, "Can't insert new settings for graveyard_backup_directory: " . db_error());
	}
}

function create_attic_share_symlink($filepath_in_attic, $attic_path) {
	global $attic_share;
	if (isset($attic_share)) {
		$filepath_in_attic = clean_dir($filepath_in_attic);
		$filepath_in_attic_share = str_replace($attic_path, $attic_share['landing_zone'], $filepath_in_attic);
		if (file_exists($filepath_in_attic_share)) {
			$new_filepath = $filepath_in_attic_share;
			$i = 1;
			while (file_exists($new_filepath)) {
				if (@readlink($new_filepath) == $filepath_in_attic) {
					// There's already a symlink to that file in the attic share; let's not make a second one!
					return;
				}
				$new_filepath = "$filepath_in_attic_share copy $i";
				$i++;
			}
			$filepath_in_attic_share = $new_filepath;
			list($original_path, $filename) = explode_full_path($filepath_in_attic_share);
		} else {
			list($original_path, $filename) = explode_full_path($filepath_in_attic);
			list($path, $filename) = explode_full_path($filepath_in_attic_share);

			$dir_infos = (object) array(
				'fileowner' => (int) gh_fileowner($original_path),
				'filegroup' => (int) gh_filegroup($original_path),
				'fileperms' => (int) base_convert("0777", 8, 10)
			);
			gh_mkdir($path, $dir_infos);
		}
		symlink($filepath_in_attic, $filepath_in_attic_share);
		gh_log(DEBUG, "  Created symlink to deleted file in {$attic_share['name']} share ($filename).");
	}
}

function process_command_line() {
	$opts_actions = 'DfClpPsSaqiU';
	$longopts_actions = array(
		'daemon',
		'fsck',
		'cancel-fsck',
		'balance',
		'prerotate',
		'postrotate',
		'stats',
		'status',
		'empty-attic',
		'view-queue',
		'iostat',
		'getuid'
	);

	$opts_actions_with_values = 'w::g::n::t::b:';
	$longopts_actions_with_values = array(
		'wait-for::',
		'gone::',
		'going::',
		'thaw::',
		'debug:'
	);

	$opts_options = 'eyucoj';
	$longopts_options = array(
		'email-report',
		'dont-walk-graveyard',
		'disk-usage-report',
		'if-conf-changed',
		'find-orphaned-files',
		'json'
	);

	$opts_options_with_values = 'd:';
	$longopts_options_with_values = array(
		'dir:'
	);

	$command_line_options = _getopt($opts_actions.$opts_actions_with_values.$opts_options.$opts_options_with_values,
		array_merge($longopts_actions, $longopts_actions_with_values, $longopts_options, $longopts_options_with_values));

	$action = 'unknown';
	$options = array();

	// Transform short options to long options
	for ($i=0; $i<mb_strlen($opts_actions); $i++) {
		if (isset($command_line_options[$opts_actions[$i]])) {
			$command_line_options[$longopts_actions[$i]] = $command_line_options[$opts_actions[$i]];
		}
	}
	$opts_actions_with_values = str_replace(':', '', $opts_actions_with_values);
	for ($i=0; $i<mb_strlen($opts_actions_with_values); $i++) {
		if (isset($command_line_options[$opts_actions_with_values[$i]])) {
			$command_line_options[str_replace(':', '', $longopts_actions_with_values[$i])] = $command_line_options[$opts_actions_with_values[$i]];
		}
	}
	for ($i=0; $i<mb_strlen($opts_options); $i++) {
		if (isset($command_line_options[$opts_options[$i]])) {
			$command_line_options[$longopts_options[$i]] = $command_line_options[$opts_options[$i]];
		}
	}
	$opts_options_with_values = str_replace(':', '', $opts_options_with_values);
	for ($i=0; $i<mb_strlen($opts_options_with_values); $i++) {
		if (isset($command_line_options[$opts_options_with_values[$i]])) {
			$command_line_options[str_replace(':', '', $longopts_options_with_values[$i])] = $command_line_options[$opts_options_with_values[$i]];
		}
	}

	// Find action and options, from command_line_options
	foreach ($longopts_actions as $longopt) {
		if (isset($command_line_options[$longopt])) {
			$action = $longopt;
			break;
		}
	}
	foreach ($longopts_actions_with_values as $longopt) {
		$longopt_action = str_replace(':', '', $longopt);
		if (isset($command_line_options[$longopt_action])) {
			$action = $longopt_action;
			if ($action == 'debug') {
				$options['debug_filename'] = $command_line_options[$longopt_action];
			} else {
				if ($command_line_options[$longopt_action] !== FALSE) {
					$options['dir'] = $command_line_options[$longopt_action];
				}
			}
			break;
		}
	}

	foreach ($longopts_options as $longopt) {
		if (isset($command_line_options[$longopt])) {
			$options[$longopt] = TRUE;
		}
	}
	foreach ($longopts_options_with_values as $longopt) {
		$longopt_option = str_replace(':', '', $longopt);
		if (isset($command_line_options[$longopt_option])) {
			$options[$longopt_option] = $command_line_options[$longopt_option];
		}
	}

	return array($action, $options);
}

function print_usage() {
	echo "greyhole, version %VERSION%, for linux-gnu (noarch)\n";
	echo "By Guillaume Boudreau.\n";
	echo "This software comes with ABSOLUTELY NO WARRANTY. This is free software,\n";
	echo "and you are welcome to modify and redistribute it under the GPL v3 license.\n";
	echo "\n";
	echo "Usage: greyhole [ACTION] [OPTIONS]\n";
	echo "\n";
	echo "Where ACTION is one of:\n";
	echo "  -?, --help            Display this help and exit.\n";
	echo "  -D, --daemon          Start the daemon.\n";
	echo "  -f, --fsck            Schedule a fsck.\n";
	echo "  -C, --cancel-fsck     Cancel all scheduled fsck.\n";
	echo "  -l, --balance         Balance available space on storage pool devices.\n";
	echo "  -s, --stats           Display storage pool statistics.\n";
	echo "  -S, --status          Display what the Greyhole daemon is currently doing.\n";
	echo "  -q, --view-queue      Display the current work queue.\n";
	echo "  -a, --empty-attic     Empty the attic.\n";
	echo "  -b, --debug=filename  Debug past file operations.\n";
	echo "  -p, --prerotate       Pre-rotate task (for logrotate).\n";
	echo "  -P, --postrotate      Post-rotate task (for logrotate).\n";
	echo "  -t, --thaw[=path]     Thaw a frozen directory. Greyhole will start working on\n";
	echo "                        files inside <path>. If you don't supply an option, the list\n";
	echo "                        of frozen directories will be displayed.\n";
	echo "  -w, --wait-for[=path] Tell Greyhole that the missing drive at <path> will return\n";
	echo "                        soon, and that it shouldn't re-create additional file copies\n";
	echo "                        to replace it. If you don't supply an option, the available\n";
	echo "                        options (paths) will be displayed.\n";
	echo "  -g, --gone[=path]     Tell Greyhole that the missing drive at <path> is gone for\n";
	echo "                        good. Greyhole will start replacing the missing file copies\n";
	echo "                        instantly. If you don't supply an option, the available\n";
	echo "                        options (paths) will be displayed.\n";
	echo "  -n, --going[=path]    Tell Greyhole that you want to remove a drive. Greyhole will\n";
	echo "                        then make sure you don't loose any files, and that the\n";
	echo "                        correct number of file copies are created to replace the\n";
	echo "                        missing drive. If you don't supply an option, the available\n";
	echo "                        options (paths) will be displayed.\n";
	echo "\n";
	echo "For --stats and --view-queue, the available OPTIONS are:\n";
	echo "  -j, --json            Output the result as JSON, instead of human-readable text.\n";
	echo "\n";
	echo "For --fsck, the available OPTIONS are:\n";
	echo "  -e, --email-report    Send an email when fsck completes, to report on what was\n";
	echo "                        checked, and any error that was found.\n";
	echo "  -y, --dont-walk-graveyard\n";
	echo "                        Speed up fsck by skipping the scan of the graveyard\n";
	echo "                        directories. Scanning the graveyards is only required to\n";
	echo "                        re-create symbolic links that might be missing from your\n";
	echo "                        shared directories.\n";
	echo "  -c, --if-conf-changed Only fsck if greyhole.conf or smb.conf paths changed since\n";
	echo "                        the last fsck.\n";
	echo "                        Used in the daily cron to prevent unneccesary fsck runs.\n";
	echo "  -d, --dir=path        Only scan a specific directory, and all sub-directories.\n";
	echo "                        The specified directory should be a Samba share, a\n";
	echo "                        sub-directory of a Samba share, a share directory\n";
	echo "                        on a storage pool volume, or a sub-directory of\n";
	echo "                        of a share on a storage pool volume.\n";
	echo "  -o, --find-orphaned-files\n";
	echo "                        Scan for orphaned files in the storage pool volumes\n";
	echo "                        while running the fsck and create tombstones for\n";
	echo "                        the found files.\n";
	exit(1);
}

function get_conf_md5() {
	global $config_file, $smb_config_file;
	exec("grep -ie 'num_copies\|storage_pool_directory\|sticky_files' " . escapeshellarg($config_file) . " | grep -v '^#'", $content);
	exec("grep -ie 'path\|vfs objects' " . escapeshellarg($smb_config_file) . " | grep -v '^#'", $content);
	return md5(implode("\n", $content));
}

function how_long_ago($past_time) {
	$ago = '';
	$s = time() - $past_time;
	$m = floor($s / 60);
	if ($m > 0) {
		$s -= $m * 60;
		$h = floor($m / 60);
		if ($h > 0) {
			$ago = $h . "h ";
			$m -= $h * 60;
		}
		$ago = $ago . $m . "m ";
	}
	$ago = $ago . $s . "s";
	return $ago;
}

// This unique identifier will be used when Greyhole calls home, to uniquely identify this Greyhole instance.
function get_uniq_id() {
	global $storage_pool_directories;

	foreach ($storage_pool_directories as $target_drive) {
		$f = "$target_drive/.greyhole_uses_this";
		if (file_exists($f) && filesize($f) == 23) {
			// Found a valid uid
			$uniq_id = file_get_contents($f);
			break;
		}
	}

	if (!isset($uniq_id)) {
		// No uid found; generate a new one.
		$uniq_id = uniqid('', TRUE);
	}
	
	return $uniq_id;
}

// Save a uid in all .greyhole_uses_this files
function set_uniq_id() {
	global $storage_pool_directories;

	$uniq_id = get_uniq_id();

	// Save the old/new uid into all .greyhole_uses_this files
	foreach ($storage_pool_directories as $target_drive) {
		$f = "$target_drive/.greyhole_uses_this";
		if (file_exists($f)) {
			file_put_contents($f, $uniq_id);
		}
	}
	
	return $uniq_id;
}
?>
